Query MongoDB - Swift SDK
On this page
- Overview
- Use Cases
- Prerequisites
- Example Data
- Async/Await Query MongoDB
- Create Documents
- Insert a Single Document
- Insert Multiple Documents
- Read Documents
- Find a Single Document
- Find Multiple Documents
- Count Documents in the Collection
- Update Documents
- Update a Single Document
- Update Multiple Documents
- Upsert Documents
- Delete Documents
- Delete a Single Document
- Delete Multiple Documents
- Aggregate Documents
- Filter Documents
- Group Documents
- Project Document Fields
- Add Fields to Documents
- Unwind Array Values
Overview
You query data stored in MongoDB directly from your client application code by using MongoDB Realm's MongoClient with the Query API. Realm provides data access rules on collections to securely retrieve results based on the logged-in user or the content of each document.
This page covers querying a MongoDB data source directly. To filter data you retrieve from Realm Database, see: Filter Data.
Use Cases
There are a variety of reasons you might want to query a MongoDB data source. Working with data in your client via Realm Sync is not always practical or possible. You might want to query MongoDB when:
- The data set is large or the client device has constraints against loading the entire data set
- You are creating or updating custom user data
- You are retrieving documents that are not modeled in Realm
- Your app needs to access collections that don't have strict schemas
- A non-Realm service generates collections that you want to access
While not exhaustive, these are some common use cases for querying MongoDB directly.
Prerequisites
Before you can remotely access MongoDB, you must:
Example Data
These examples operate on a MongoDB collection that describes coffee drinks in a chain of coffee shops. The documents represent objects with these properties:
class CoffeeDrink: Object { true) var _id: ObjectId (primaryKey: var name: String var beanRegion: String? var containsDairy: Bool var _partition: String }
The complete code for each example includes logging in and instantiating a MongoDB collection handle before completing each operation. For brevity, these examples omit the login and collection handle code. However, each complete example looks like this:
app.login(credentials: Credentials.anonymous) { (result) in // Remember to dispatch back to the main thread in completion handlers // if you want to do anything on the UI. DispatchQueue.main.async { switch result { case .failure(let error): print("Login failed: \(error)") case .success(let user): print("Login as \(user) succeeded!") // Continue below } // mongodb-atlas is the cluster service name let client = app.currentUser!.mongoClient("mongodb-atlas") // Select the database let database = client.database(named: "ios") // Select the collection let collection = database.collection(withName: "CoffeeDrinks") let drink: Document = [ "name": "Bean of the Day", "beanRegion": "Timbio, Colombia", "containsDairy": "false", "partition": "Store 42"] let drink2: Document = [ "name": "Maple Latte", "beanRegion": "Yirgacheffe, Ethiopia", "containsDairy": "true", "partition": "Store 42"] let drink3: Document = [ "name": "Bean of the Day", "beanRegion": "San Marcos, Guatemala", "containsDairy": "false", "partition": "Store 47"] // Insert the documents into the collection collection.insertMany([drink, drink2, drink3]) { result in switch result { case .failure(let error): print("Call to MongoDB failed: \(error.localizedDescription)") return case .success(let objectIds): print("Successfully inserted \(objectIds.count) new documents.") } } } }
Async/Await Query MongoDB
New in version 10.16.0.
The Realm Swift SDK provides async/await versions of the MongoCollection methods.
All of the methods on this page are compatible with the async/await syntax.
This example illustrates that syntax for the collection.insertOne()
method.
You can see the completion handler version in Insert a Single Document.
// This document represents a CoffeeDrink object let drink: Document = [ "name": "Bean of the Day", "beanRegion": "Timbio, Colombia", "containsDairy": "false", "_partition": "Store 43"] do { // Use the async collection method to insert the document let objectId = try await collection.insertOne(drink) print("Successfully inserted a document with id: \(objectId)") } catch { print("Call to MongoDB failed: \(error.localizedDescription)") }
Starting with Realm Swift SDK Versions 10.15.0 and 10.16.0, many of the Realm APIs support the Swift async/await syntax. Projects must meet these requirements:
Swift SDK Version | Swift Version Requirement | Supported OS |
---|---|---|
10.25.0 | Swift 5.6 | iOS 13.x |
10.15.0 or 10.16.0 | Swift 5.5 | iOS 15.x |
Create Documents
These code snippets demonstrate how to insert one or more documents into a MongoDB collection from a mobile application. These methods take one or more documents and return a result. Success returns the objectId of the inserted document, or an array of objectIds in order when inserting multiple documents.
Insert a Single Document
You can insert a single document using collection.insertOne().
This snippet inserts a single document describing a "Bean of the Day" coffee drink into a collection of documents that describe coffee drinks for sale in a group of stores:
// This document represents a CoffeeDrink object let drink: Document = [ "name": "Bean of the Day", "beanRegion": "Timbio, Colombia", "containsDairy": "false", "_partition": "Store 43"] // Insert the document into the collection collection.insertOne(drink) { result in switch result { case .failure(let error): print("Call to MongoDB failed: \(error.localizedDescription)") return case .success(let objectId): // Success returns the objectId for the inserted document print("Successfully inserted a document with id: \(objectId)") } }
Running this snippet produces this output:
Successfully inserted a document with id: objectId(6108...)
Insert Multiple Documents
You can insert multiple documents using collection.insertMany().
This snippet inserts three documents describing coffee drinks into a collection of documents that describe coffee drinks for sale in a group of stores:
let drink: Document = [ "name": "Bean of the Day", "beanRegion": "Timbio, Colombia", "containsDairy": "false", "_partition": "Store 42"] let drink2: Document = [ "name": "Maple Latte", "beanRegion": "Yirgacheffe, Ethiopia", "containsDairy": "true", "_partition": "Store 42"] let drink3: Document = [ "name": "Bean of the Day", "beanRegion": "San Marcos, Guatemala", "containsDairy": "false", "_partition": "Store 47"] // Insert the documents into the collection collection.insertMany([drink, drink2, drink3]) { result in switch result { case .failure(let error): print("Call to MongoDB failed: \(error.localizedDescription)") return case .success(let objectIds): print("Successfully inserted \(objectIds.count) new documents.") } }
Running this snippet produces this output:
Successfully inserted 3 new documents.
Read Documents
These code snippets demonstrate how to read data stored in a MongoDB collection
from a mobile application. Read operations use a standard query syntax to specify which documents to return
from the database. Read operations return a result that that resolves to
either a single matched document (in the case of findOneDocument()
), a
long
numeric value (in the case of count()
) or an array of matched
documents (in the case of find()
).
Find a Single Document
You can find a single document using collection.findOneDocument().
This snippet finds a single document from a collection of documents
that describe coffee drinks for sale in a group of stores, where the document's name
field contains
the string value "Maple Latte":
let queryFilter: Document = ["name": "Maple Latte"] collection.findOneDocument(filter: queryFilter) { result in switch result { case .failure(let error): print("Did not find matching documents: \(error.localizedDescription)") return case .success(let document): print("Found a matching document: \(document)") } }
Running this snippet produces this output:
Found a matching document: Optional([ "name": Optional(RealmSwift.AnyBSON.string("Maple Latte")), "_id": Optional(RealmSwift.AnyBSON.objectId(60f5...)), "creamer": Optional(RealmSwift.AnyBSON.string("true")), "partition": Optional(RealmSwift.AnyBSON.string("Store 42")), "beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia"))])
Find Multiple Documents
You can find multiple documents using collection.find().
This snippet finds all documents in a collection of documents that
describe coffee drinks for sale in a group of stores,
where the document's name
field contains the value "Americano":
let queryFilter: Document = ["name": "Americano"] collection.find(filter: queryFilter) { result in switch result { case .failure(let error): print("Call to MongoDB failed: \(error.localizedDescription)") return case .success(let documents): print("Results: ") for document in documents { print("Coffee drink: \(document)") } } }
Running this snippet produces this output:
Results: Coffee drink: [ "partition": Optional(RealmSwift.AnyBSON.string("Store 42")), "creamer": Optional(RealmSwift.AnyBSON.string("false")), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia")), "_id": Optional(RealmSwift.AnyBSON.objectId(6102...)), "name": Optional(RealmSwift.AnyBSON.string("Americano"))] Coffee drink: [ "creamer": Optional(RealmSwift.AnyBSON.string("false")), "name": Optional(RealmSwift.AnyBSON.string("Americano")), "partition": Optional(RealmSwift.AnyBSON.string("Store 47")), "_id": Optional(RealmSwift.AnyBSON.objectId(6102...)), "beanRegion": Optional(RealmSwift.AnyBSON.string("San Marcos, Guatemala"))]
Count Documents in the Collection
You can count documents in a collection using collection.count(). You can specify an optional query and limit to determine which documents to count. If you don't specify a query, the action counts all documents in the collection.
This snippet counts the number of documents in a collection of documents
that describe coffee drinks for sale in a group of stores, where the document's name
field contains
the value "Bean of the Day":
let queryFilter: Document = ["name": "Bean of the Day"] collection.count(filter: queryFilter) { result in switch result { case .failure(let error): print("Call to MongoDB failed: \(error.localizedDescription)") return case .success(let count): print("Found this many documents in the collection matching the filter: \(count)") } }
Running this snippet produces this output:
Found this many documents in the collection matching the filter: 24
Update Documents
These code snippets demonstrate how to update data stored in a MongoDB
collection from a mobile application. Update operations use queries to
specify which documents to update, and update operators to desribe how to mutate documents that match
the query. Update operations return a result that resolves to an
UpdateResult
or Error
.
Update a Single Document
You can update a single document using collection.updateOneDocument().
This snippet updates a single document in a collection of documents
that describe coffee drinks for sale in a group of stores. This update operation queries for a document
whose name
field contains the value "Bean of the Day", and sets the
containsDairy
field to "true":
let queryFilter: Document = ["name": "Bean of the Day", "_partition": "Store 42"] let documentUpdate: Document = ["$set": ["containsDairy": "true"]] collection.updateOneDocument(filter: queryFilter, update: documentUpdate) { result in switch result { case .failure(let error): print("Failed to update document: \(error.localizedDescription)") return case .success(let updateResult): if updateResult.matchedCount == 1 && updateResult.modifiedCount == 1 { print("Successfully updated a matching document.") } else { print("Did not update a document") } }
Running this snippet produces this output:
Successfully updated a matching document.
Update Multiple Documents
You can update multiple documents using collection.updateManyDocuments().
This snippet updates multiple documents in a collection of documents
that describe coffee drinks for sale in a group of stores. This update operation queries for documents
where the name
field contains the value "Bean of the Day", and changes the
containsDairy
field to "true":
let queryFilter: Document = ["name": "Bean of the Day"] let documentUpdate: Document = ["$set": ["containsDairy": "true"]] collection.updateManyDocuments(filter: queryFilter, update: documentUpdate) { result in switch result { case .failure(let error): print("Failed to update document: \(error.localizedDescription)") return case .success(let updateResult): print("Successfully updated \(updateResult.modifiedCount) documents.") } }
Running this snippet produces this output:
Successfully updated 24 documents.
Upsert Documents
If an update operation does not match any document in the collection,
you can automatically insert a single new document into the collection
that matches the update query by setting the upsert
option to
true
.
The following snippet updates a document in a collection of documents
that describe coffee drinks for sale in a group of stores. If no document matches the query, it
inserts a new document if no document. This operation queries for documents
where the name
field has a value of "Bean of the Day", and
the partition
field has a value of "Store 55".
Because this snippet sets the upsert
option to true
, if no
document matches the query, MongoDB creates a new document that includes
both the query and specified updates:
let queryFilter: Document = ["name": "Bean of the Day", "_partition": "Store 55"] let documentUpdate: Document = ["name": "Bean of the Day", "beanRegion": "Yirgacheffe, Ethiopia", "containsDairy": "false", "_partition": "Store 55"] collection.updateOneDocument(filter: queryFilter, update: documentUpdate, upsert: true) { result in switch result { case .failure(let error): print("Failed to update document: \(error.localizedDescription)") return case .success(let updateResult): if updateResult.objectId != nil { print("Successfully upserted a document with id: \(updateResult.objectId)") } else { print("Did not upsert a document") } } }
Running this snippet produces this output:
Successfully upserted a document with id: Optional(6108...)
Delete Documents
These code snippets demonstrate how to delete documents that
are stored in a MongoDB collection from a mobile application.
Delete operations use a query to specify which documents to delete
and return results that resolve to an Int
count of deleted documents
or Error
.
Delete a Single Document
You can delete a single document from a collection using collection.deleteOneDocument().
This snippet deletes one document in a collection of documents
that describe coffee drinks for sale in a group of stores. This operation queries for a document where
the name
field has a value of "Mocha" and the partition
field has
a value of "Store 17", and deletes it.
let queryFilter: Document = ["name": "Mocha", "_partition": "Store 17"] collection.deleteOneDocument(filter: queryFilter) { deletedResult in switch deletedResult { case .failure(let error): print("Failed to delete a document: \(error.localizedDescription)") return case .success(let deletedResult): print("Successfully deleted a document.") } }
Running this snippet produces this output:
Successfully deleted a document.
Delete Multiple Documents
You can delete multiple items from a collection using collection.deleteManyDocuments().
This snippet deletes all documents in a collection of documents
that describe coffee drinks for sale in a group of stores that match the query for documents
whose name
field contains the value "Caramel Latte":
let filter: Document = ["name": "Caramel Latte"] collection.deleteManyDocuments(filter: filter) { deletedResult in switch deletedResult { case .failure(let error): print("Failed to delete a document: \(error.localizedDescription)") return case .success(let deletedResult): print("Successfully deleted \(deletedResult) documents.") } }
Running this snippet produces this output:
Successfully deleted 3 documents.
Aggregate Documents
Aggregation operations run all documents in a collection through a series of data aggregation stages called an aggregation pipeline. Aggregation allows you to filter and transform documents, collect summary data about groups of related documents, and other complex data operations.
You can configure and run aggregation operations on a collection using collection.aggregate().
An aggregation operation accepts a list of aggregation stages as input
and returns a result that resolves to a collection of documents processed
by the pipeline, or an Error
.
Filter Documents
You can use the $match stage to filter documents using standard MongoDB query syntax:
This $match
stage filters documents to include only those where the
partition
field has a value equal to "Store 42":
let pipeline: [Document] = [["$match": ["partition": ["$eq": "Store 42"]]]] collection.aggregate(pipeline: pipeline) { result in switch result { case .failure(let error): print("Failed to aggregate: \(error.localizedDescription)") return case .success(let documents): print("Successfully ran the aggregation:") for document in documents { print("Coffee drink: \(document)") } } }
Running this snippet produces this output:
Successfully ran the aggregation: Coffee drink: [ "partition": Optional(RealmSwift.AnyBSON.string("Store 42")), "creamer": Optional(RealmSwift.AnyBSON.string("false")), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia")), "_id": Optional(RealmSwift.AnyBSON.objectId(6102...)), "name": Optional(RealmSwift.AnyBSON.string("Americano"))] Coffee drink: [ "creamer": Optional(RealmSwift.AnyBSON.string("false")), "name": Optional(RealmSwift.AnyBSON.string("Americano")), "partition": Optional(RealmSwift.AnyBSON.string("Store 42")), "_id": Optional(RealmSwift.AnyBSON.objectId(6102...)), "beanRegion": Optional(RealmSwift.AnyBSON.string("San Marcos, Guatemala"))] (...more results...)
Group Documents
You can use the $group stage to aggregate summary
data for one or more documents. MongoDB groups documents based
on the expression defined in the _id
field of the $group
stage.
You can reference a specific document field by prefixing the field name
with a $
.
This $group
stage arranges documents by the value of their
partition
field, which in this case is the store number. It then
calculates the number of coffee drink documents that contain that store number
as the value of the partition
field. In other words, we're calculating
the number of coffee drinks for each store number.
let pipeline: [Document] = [["$group": ["_id": "$partition", "numItems": ["$sum": 1]]]] collection.aggregate(pipeline: pipeline) { result in switch result { case .failure(let error): print("Failed to aggregate: \(error.localizedDescription)") return case .success(let results): print("Successfully ran the aggregation.") for result in results { print(result) } } }
Running this snippet produces this output:
Successfully ran the aggregation. ["numItems": Optional(RealmSwift.AnyBSON.int64(6)), "_id": Optional(RealmSwift.AnyBSON.string("Store 43"))] ["numItems": Optional(RealmSwift.AnyBSON.int64(16)), "_id": Optional(RealmSwift.AnyBSON.string("Store 42"))] ["_id": Optional(RealmSwift.AnyBSON.string("Store 47")), "numItems": Optional(RealmSwift.AnyBSON.int64(7))] (...more results...)
Project Document Fields
You can use the $project stage to include or omit specific fields from documents or to calculate new fields using aggregation operators. Projections work in two ways:
- Specify that you want to include fields by using a
1
. This has the side-effect of implicitly excluding all unspecified fields. - Specify that you want to exclude fields by using a
0
. This has the side-effect of implicitly including all unspecified fields.
These two methods of projection are mutually exclusive. If you specify fields to include, you cannot also specify fields to exclude, and vice versa.
The _id
field is a special case: it is always included in every
query unless explicitly specified otherwise. For this reason, you
can exclude the _id
field with a 0
value while simultaneously
including other fields, like _partition
, with a 1
. Only the
special case of exclusion of the _id
field allows both exclusion
and inclusion in one $project
stage.
The following $project
stage omits the _id
field, includes
the name
field, and creates a new field named storeNumber
.
The storeNumber
is generated using two aggregation operators:
$split
separates thepartition
value into two string segments surrounding the space character. For example, the value "Store 42" split in this way returns an array with two elements: "Store" and "42".$arrayElemAt
selects a specific element from an array based on the second argument. In this case, the value1
selects the second element from the array generated by the$split
operator since arrays index from0
. For example, the value ["Store", "42"] passed to this operation would return a value of "42".
let pipeline: [Document] = [["$project": ["_id": 0, "name": 1, "storeNumber": ["$arrayElemAt": [["$split": ["$partition", " "]], 1]]]]] collection.aggregate(pipeline: pipeline) { result in switch result { case .failure(let error): print("Failed to aggregate: \(error.localizedDescription)") return case .success(let results): print("Successfully ran the aggregation.") for result in results { print(result) } } }
Running this snippet produces this output:
Successfully ran the aggregation. ["name": Optional(RealmSwift.AnyBSON.string("Maple Latte")), "storeNumber": Optional(RealmSwift.AnyBSON.string("42"))] ["storeNumber": Optional(RealmSwift.AnyBSON.string("42")), "name": Optional(RealmSwift.AnyBSON.string("Americano"))] ["name": Optional(RealmSwift.AnyBSON.string("Americano")), "storeNumber": Optional(RealmSwift.AnyBSON.string("47"))] (...more results...)
Add Fields to Documents
You can use the $addFields stage to add new fields with calculated values using aggregation operators.
$addFields
is similar to $project but does not allow you to
include or omit fields.
The following $addFields
stage creates a new field named
storeNumber
where the value is the output of two aggregate operators
that transform the value of the partition
field.
let pipeline: [Document] = [["$addFields": ["storeNumber": ["$arrayElemAt": [["$split": ["$partition", " "]], 1]]]]] collection.aggregate(pipeline: pipeline) { result in switch result { case .failure(let error): print("Failed to aggregate: \(error.localizedDescription)") return case .success(let results): print("Successfully ran the aggregation.") for result in results { print(result) } } }
Running this snippet produces this output:
Successfully ran the aggregation. ["storeNumber": Optional(RealmSwift.AnyBSON.string("42")), "creamer": Optional(RealmSwift.AnyBSON.string("true")), "_id": Optional(RealmSwift.AnyBSON.objectId(60f5f39f1eb0f39071acef87)), "name": Optional(RealmSwift.AnyBSON.string("Maple Latte")), "beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia")), "partition": Optional(RealmSwift.AnyBSON.string("Store 42"))] ["beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia")), "storeNumber": Optional(RealmSwift.AnyBSON.string("42")), "_id": Optional(RealmSwift.AnyBSON.objectId(6102b91aaa4f3fc37642119e)), "name": Optional(RealmSwift.AnyBSON.string("Maple Latte")), "creamer": Optional(RealmSwift.AnyBSON.string("true")), "partition": Optional(RealmSwift.AnyBSON.string("Store 42"))] ["creamer": Optional(RealmSwift.AnyBSON.string("false")), "_id": Optional(RealmSwift.AnyBSON.objectId(6102f577099eb9b818497908)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia")), "storeNumber": Optional(RealmSwift.AnyBSON.string("42")), "partition": Optional(RealmSwift.AnyBSON.string("Store 42")), "name": Optional(RealmSwift.AnyBSON.string("Americano"))] (...more results...)
Unwind Array Values
You can use the $unwind stage to transform a single document containing an array into multiple documents containing individual values from that array. When you unwind an array field, MongoDB copies each document once for each element of the array field but replaces the array value with the array element in each copy.
The following $unwind
stage creates a new document for each
element of the items
array in each document. It also adds a field
called itemIndex
to each new document that specifies the
element's position index in the original array:
let pipeline: [Document] = [["$unwind": ["path": "$featuredInPromotions", "includeArrayIndex": "itemIndex"]]] collection.aggregate(pipeline: pipeline) { result in switch result { case .failure(let error): print("Failed to aggregate: \(error.localizedDescription)") return case .success(let results): print("Successfully ran the aggregation.") for result in results { print("Coffee drink: \(result)") } } }
Consider this document that includes a featuredInPromotions
array:
_id: 610802b44386a9ed3144447d, name: "Maple Latte", containsDairy:"true", partition:"Store 42", beanRegion: "Yirgacheffe, Ethiopia", featuredInPromotions: [ "Spring into Spring", "Tastes of Fall", "Winter Delights" ]
If we apply the example $unwind
stage to this document, the stage outputs
the following three documents:
Successfully ran the aggregation. Coffee drink: [ "featuredInPromotions": Optional(RealmSwift.AnyBSON.string("Spring into Spring")), "itemIndex": Optional(RealmSwift.AnyBSON.int64(0)), "name": Optional(RealmSwift.AnyBSON.string("Maple Latte")), "beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia")), "containsDairy": Optional(RealmSwift.AnyBSON.string("true")), "partition": Optional(RealmSwift.AnyBSON.string("Store 42")), "_id": Optional(RealmSwift.AnyBSON.objectId(6108...))] Coffee drink: [ "featuredInPromotions": Optional(RealmSwift.AnyBSON.string("Tastes of Fall")), "itemIndex": Optional(RealmSwift.AnyBSON.int64(1)), "name": Optional(RealmSwift.AnyBSON.string("Maple Latte")), "containsDairy": Optional(RealmSwift.AnyBSON.string("true")), "beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia")), "partition": Optional(RealmSwift.AnyBSON.string("Store 42")), "_id": Optional(RealmSwift.AnyBSON.objectId(6108...))] Coffee drink: [ "featuredInPromotions": Optional(RealmSwift.AnyBSON.string("Winter Delights")), "itemIndex": Optional(RealmSwift.AnyBSON.int64(2)), "partition": Optional(RealmSwift.AnyBSON.string("Store 42")), "containsDairy": Optional(RealmSwift.AnyBSON.string("true")), "beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia")), "_id": Optional(RealmSwift.AnyBSON.objectId(6108...)), "name": Optional(RealmSwift.AnyBSON.string("Maple Latte"))]
You could then $group
by the value of featuredInPromotions
and $sum
the number of coffee drinks in each promotion as in the group documents
example, or perform other calculations or
transformations based on your data.