查询 MongoDB — Swift SDK
在此页面上
Overview
您可以使用Realm Swift SDK 的 MongoClient和Query API直接从客户端应用程序代码查询存储在MongoDB中的数据。 Atlas App Services提供集合的数据访问规则,以便根据登录用户或每个文档的内容安全地检索结果。
用例
您可能出于多种原因想要查询 MongoDB 数据源。通过 Atlas Device Sync 处理客户端中的数据并不总是可行或可能的。您可能希望在以下情况下查询 MongoDB:
数据集较大或客户端设备有限制,无法加载整个数据集
您正在创建或更新自定义用户数据
您正在检索未在 Realm 中建模的文档
您的应用程序需要访问没有严格模式的集合
非 Realm 服务会生成要访问的集合
以上查询场景虽未穷尽,却是直接查询 MongoDB 的一些常见使用案例。
先决条件
从客户端应用程序查询 MongoDB 之前,必须在 App Services App 中设置 MongoDB 数据访问权限。 要了解如何设置后端应用程序以使Realm SDK 查询Atlas ,请参阅 MongoDBAtlas App Services文档中的 设置 数据访问权限 。
示例数据
这些示例在 MongoDB 集合上运行,该集合描述了连锁咖啡店的咖啡饮料。 文档表示具有以下属性的对象:
class CoffeeDrink: Object { true) var _id: ObjectId (primaryKey: var name: String var beanRegion: String? var containsDairy: Bool var storeNumber: Int }
每个示例的完整代码包括在完成每个操作之前登录和实例化 MongoDB 集合句柄。 为简洁起见,这些示例省略了登录和collection句柄代码。但是,每个完整示例如下所示:
appClient.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!") // mongodb-atlas is the cluster service name let mongoClient = user.mongoClient("mongodb-atlas") // Select the database let database = mongoClient.database(named: "ios") // Select the collection let collection = database.collection(withName: "CoffeeDrinks") // This document represents a CoffeeDrink object let drink: Document = [ "name": "Colombian Blend", "beanRegion": "Timbio, Colombia", "containsDairy": false, "storeNumber": 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)") } } } } }
异步/等待查询 MongoDB
版本 10.16.0 中的新增内容。
Realm Swift SDK 提供了 MongoCollection 方法的异步/等待版本。
此页面上的所有方法都与异步/等待语法兼容。 此示例说明了 collection.insertOne()
方法的语法。 您可以在插入单个文档中查看完成处理程序版本。
// This document represents a CoffeeDrink object let drink: Document = [ "name": "Bean of the Day", "beanRegion": "Timbio, Colombia", "containsDairy": false, "storeNumber": 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)") }
从 Realm Swift SDK 10.15.0 和 10.16.0 版本开始,很多 Realm API 均支持 Swift“异步/等待”语法。项目必须符合以下要求:
Swift SDK 版本 | Swift 版本要求 | 支持的操作系统 |
---|---|---|
10.25.0 | Swift 5.6 | iOS 13.x |
10.15.0 或 10.16.0 | Swift 5.5 | iOS 15.x |
如果您的应用会在 async/await
上下文中访问 Realm,请使用 @MainActor
来标记此代码,从而避免出现与线程相关的崩溃。
创建文档
这些代码片段演示了如何从移动应用程序将一个或多个文档插入 MongoDB 集合。 这些方法获取一个或多个文档并返回结果。 插入多个文档时,成功会返回已插入文档的ObjectId ,或按顺序返回objectId 数组。
插入单一文档
您可以使用collection.insertOne() 插入单个文档。
此代码段将描述“Colombian Blend”咖啡饮料的单个文档插入到描述一组商店中出售的咖啡饮料的文档集合中:
// This document represents a CoffeeDrink object let drink: Document = [ "name": "Colombian Blend", "beanRegion": "Timbio, Colombia", "containsDairy": false, "storeNumber": 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)") } }
Successfully inserted a document with id: objectId(64e50cab7765243942cd04ce)
插入多个文档
您可以使用collection.insertMany() 插入多个文档。
此代码片段将三个描述咖啡饮料的文档插入到描述一组商店中待售咖啡饮料的文档集合中:
let drink: Document = [ "name": "Bean of the Day", "beanRegion": "Timbio, Colombia", "containsDairy": false, "storeNumber": 42] let drink2: Document = [ "name": "Maple Latte", "beanRegion": "Yirgacheffe, Ethiopia", "containsDairy": true, "storeNumber": 42] let drink3: Document = [ "name": "Bean of the Day", "beanRegion": "San Marcos, Guatemala", "containsDairy": false, "storeNumber": 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.") } }
Successfully inserted 3 new documents.
读取文档
这些代码片段演示了如何从移动应用程序读取存储在 MongoDB 集合中的数据。 读取操作使用标准查询语法来指定要从数据库返回哪些文档。 读取操作返回一个结果,该结果解析为单个匹配文档(在findOneDocument()
的情况下)、 long
数值(在count()
的情况下)或匹配文档的数组(在 的情况下) find()
)。
查找单个文档
您可以使用collection.findOneDocument() 查找单个文档。
此代码段从描述一组商店中出售的咖啡饮料的文档集合中查找单个文档,其中该文档的 name
字段包含string值“Colombian Blend”:
let queryFilter: Document = ["name": "Colombian Blend"] 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: \(String(describing: document))") } }
Found a matching document: Optional([ "name": Optional(RealmSwift.AnyBSON.string("Colombian Blend")), "_id": Optional(RealmSwift.AnyBSON.objectId(64e5014f65796c813bc68274)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia")), "containsDairy": Optional(RealmSwift.AnyBSON.bool(false)), "storeNumber": Optional(RealmSwift.AnyBSON.int64(43)) ])
查找多个文档
您可以使用collection.find() 查找多个文档。
此代码段查找描述一组商店中出售的咖啡饮料的文档集合中的所有文档,其中文档的name
字段包含值“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 bsonDocumentArray): print("Results: ") for bsonDocument in bsonDocumentArray { print("Coffee drink named: \(String(describing: bsonDocument["name"]))") } } }
Results: Coffee drink named: Optional(Optional(RealmSwift.AnyBSON.string("Americano"))) ... more matching documents ...
查找文档并对其进行排序
可以通过使用首选排序选项初始化FindOptions实例,对集合中的文档进行排序。 FindOptions
具有三个参数: limit 、 projection和sorting 。 sorting
参数可以是键值对数组,其中每个键代表一个字段。 对于每个键,值1
按降序排序,或-1
按升序排序。
然后,在运行查询时,将FindOptions
实例传递给collection.find()
方法。
此代码段查找描述一组商店中出售的咖啡饮料的文档集合中的所有文档,其中该文档的name
字段包含值“Americano”,该文档按beanRegion
字段降序排序:
let queryFilter: Document = ["name": "Americano"] let findOptions = FindOptions(0, nil, [["beanRegion": 1]]) collection.find(filter: queryFilter, options: findOptions) { 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)") } } }
Results: Coffee drink: [ "_id": Optional(RealmSwift.AnyBSON.objectId(64e521ed6b124fd047534345)), "beanRegion": Optional(RealmSwift.AnyBSON.string("San Marcos, Guatemala")), "name": Optional(RealmSwift.AnyBSON.string("Americano")), "containsDairy": Optional(RealmSwift.AnyBSON.bool(true)), "storeNumber": Optional(RealmSwift.AnyBSON.int64(42)) ] Coffee drink: [ "_id": Optional(RealmSwift.AnyBSON.objectId(64e521ed6b124fd047534344)), "storeNumber": Optional(RealmSwift.AnyBSON.int64(42)), "containsDairy": Optional(RealmSwift.AnyBSON.bool(false)), "name": Optional(RealmSwift.AnyBSON.string("Americano")), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia")) ] ... more matching documents, sorted by `beanRegion` ...
对集合中的文档进行计数
您可以使用collection.count()对集合中的文档进行计数。 您可以指定可选的查询和限制,以确定要计数的文档。 如果未指定查询,则该操作将对集合中的所有文档进行计数。
此代码段计算描述一组商店中出售的咖啡饮料的文档集合中的文档数量,其中文档的name
字段包含值“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)") } }
Found this many documents in the collection matching the filter: 24
Update Documents
这些代码片段演示了如何从移动应用程序更新存储在 MongoDB 集合中的数据。 更新操作使用查询来指定要更新的文档,并使用更新操作符来描述如何更改与查询匹配的文档。 更新操作返回解析为UpdateResult或Error
的结果。
更新单份文档
您可以使用collection.updateOneDocument() 更新单个文档。
此代码段更新描述一组商店中待售咖啡饮料的文档集合中的单个文档。 此更新操作查询name
字段包含值“Bean of the Day”的文档,并将containsDairy
字段设置为true
:
let queryFilter: Document = ["name": "Bean of the Day", "storeNumber": 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") } }
Successfully updated a matching document.
更新多个文档
您可以使用collection.updateManyDocuments() 更新多个文档。
此代码段更新文档集合中的多个文档,这些文档描述一组商店中出售的咖啡饮料。 此更新操作查询name
字段包含值“Bean of the Day”的文档,并将containsDairy
字段更改为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.") } }
Successfully updated 24 documents.
更新或插入文档
如果更新操作与集合中的任何文档都不匹配,则您可以通过将 upsert
选项设置为 true
,自动将单个新文档插入到与更新查询匹配的集合。
以下代码段更新文档集合中的一个文档,这些文档描述一组商店中出售的咖啡饮料。 如果没有与查询匹配的文档,则插入一个新文档。 此操作查询name
字段值为“Bean of the Day”且storeNumber
字段值为55
的文档。
由于该代码片段将 upsert
选项设置为 true
,如果没有与查询匹配的文档,MongoDB 就会创建新文档,其中包括查询和指定的更新:
let queryFilter: Document = ["name": "Bean of the Day", "storeNumber": 55] let documentUpdate: Document = ["name": "Bean of the Day", "beanRegion": "Yirgacheffe, Ethiopia", "containsDairy": false, "storeNumber": 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 let unwrappedDocumentId = updateResult.documentId { print("Successfully upserted a document with id: \(unwrappedDocumentId)") } else { print("Did not upsert a document") } } }
Successfully upserted a document with id: 64e523e37765243942eba44a
Delete Documents
这些代码片段演示了如何从移动应用程序中删除存储在 MongoDB collection中的文档。删除操作使用查询来指定要删除的文档,并返回解析为Int
已删除文档的计数或Error
的结果。
删除单个文档
您可以使用collection.deleteOneDocument() 从集合中删除单个文档。
此代码段删除描述一组商店中出售的咖啡饮料的文档集合中的一个文档。 此操作查询name
字段值为 "Mocha" 且storeNumber
字段值为17
的文档,并将其删除。
let queryFilter: Document = ["name": "Mocha", "storeNumber": 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.") } }
Successfully deleted a document.
删除多个文档
您可以使用collection.deleteManyDocuments() 从集合中删除多个项目。
此代码段删除描述一组商店中出售的咖啡饮料的文档集合中的所有文档,这些文档与name
字段包含值“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.") } }
Successfully deleted 3 documents.
注意更改
您可以监视集合,以了解每当创建、修改或删除集合中的文档时 MongoDB 发出的变更通知。 每个通知都会指定已更改的文档、更改方式以及导致事件的操作后的完整文档。
重要
无服务器限制
如果数据源为 Atlas 无服务器实例,则无法监控是否出现更改。MongoDB 无服务器当前不支持变更流,而变更流可用于受监视的集合以侦听是否存在更改。
观察集合中的所有更改
您可以通过调用collection.watch()打开对集合所做的更改流。 此函数创建一个发布者,当 MongoDB 集合发生更改时,该发布者会发出 AnyBSON 更改事件。
您可以选择使用 collection.watch(filterIds:) 监视集合中_ids
的筛选列表,或使用collection.watch(matchFilter:)将$match
筛选器应用于传入的变更事件。
.watch()
方法可以采用ChangeEventDelegate来订阅流上的更改。 例如,对于此ChangeEventDelegate
:
class MyChangeStreamDelegate: ChangeEventDelegate { func changeStreamDidOpen(_ changeStream: RealmSwift.ChangeStream) { print("Change stream opened: \(changeStream)") } func changeStreamDidClose(with error: Error?) { if let anError = error { print("Change stream closed with error: \(anError.localizedDescription)") } else { print("Change stream closed") } } func changeStreamDidReceive(error: Error) { print("Received error: \(error.localizedDescription)") } func changeStreamDidReceive(changeEvent: RealmSwift.AnyBSON?) { guard let changeEvent = changeEvent else { return } guard let document = changeEvent.documentValue else { return } print("Change event document received: \(document)") } }
CoffeeDrinks
此代码监视collection中的文档更改:
appClient.login(credentials: Credentials.anonymous) { (result) in DispatchQueue.main.async { switch result { case .failure(let error): print("Login failed: \(error)") case .success(let user): print("Login as \(user) succeeded!") // Continue below } // Set up the client, database, and collection. let client = self.appClient.currentUser!.mongoClient("mongodb-atlas") let database = client.database(named: "ios") let collection = database.collection(withName: "CoffeeDrinks") // Watch the collection. In this example, we use a queue and delegate, // both of which are optional arguments. let queue = DispatchQueue(label: "io.realm.watchQueue") let delegate = MyChangeStreamDelegate() let changeStream = collection.watch(delegate: delegate, queue: queue) // Adding a document triggers a change event. let drink: Document = [ "name": "Bean of the Day", "beanRegion": "Timbio, Colombia", "containsDairy": false, "storeNumber": 42] collection.insertOne(drink) { result in switch result { case .failure(let error): print("Call to MongoDB failed: \(error.localizedDescription)") return case .success(let objectId): print("Successfully inserted a document with id: \(objectId)") } } // After you're done watching for events, close the change stream. changeStream.close() } }
Login as <RLMUser: 0x600002dfd1a0> succeeded! Change stream opened: <RLMChangeStream: 0x60000182ab80> Successfully inserted a document with id: objectId(64e525665fef1743dedb5aa6) Change event document received: [ "clusterTime": Optional(RealmSwift.AnyBSON.datetime(2023-08-22 21:15:18 +0000)), "_id": Optional(RealmSwift.AnyBSON.document([ "_data": Optional(RealmSwift.AnyBSON.string( "8264E525660000000B2B022C0100296E5A100464816C3449884434A07AC19F4AAFCB8046645F6964006464E525665FEF1743DEDB5AA60004" )) ])), "documentKey": Optional(RealmSwift.AnyBSON.document([ "_id": Optional(RealmSwift.AnyBSON.objectId(64e525665fef1743dedb5aa6)) ])), "ns": Optional(RealmSwift.AnyBSON.document([ "coll": Optional(RealmSwift.AnyBSON.string("CoffeeDrinks")), "db": Optional(RealmSwift.AnyBSON.string("ios")) ])), "operationType": Optional(RealmSwift.AnyBSON.string("insert")), "fullDocument": Optional(RealmSwift.AnyBSON.document([ "name": Optional(RealmSwift.AnyBSON.string("Bean of the Day")), "storeNumber": Optional(RealmSwift.AnyBSON.int64(42)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia")), "_id": Optional(RealmSwift.AnyBSON.objectId(64e525665fef1743dedb5aa6)), "containsDairy": Optional(RealmSwift.AnyBSON.bool(false)) ]))] Change stream closed
监视集合中对特定 ID 的更改
您可以通过传入_id
来监视集合中特定对象列表的更改。 使用 ObjectId 数组调用collection.watch(filterIds: )以仅接收适用于这些文档的更改事件。
class MyChangeStreamDelegate: ChangeEventDelegate { func changeStreamDidOpen(_ changeStream: RealmSwift.ChangeStream) { print("Change stream opened: \(changeStream)") } func changeStreamDidClose(with error: Error?) { if let anError = error { print("Change stream closed with error: \(anError.localizedDescription)") } else { print("Change stream closed") } } func changeStreamDidReceive(error: Error) { print("Received error: \(error.localizedDescription)") } func changeStreamDidReceive(changeEvent: RealmSwift.AnyBSON?) { guard let changeEvent = changeEvent else { return } guard let document = changeEvent.documentValue else { return } print("Change event document received: \(document)") } }
CoffeeDrinks
以下代码使用此委托监视collection中特定文档的更改:
appClient.login(credentials: Credentials.anonymous) { (result) in DispatchQueue.main.async { switch result { case .failure(let error): print("Login failed: \(error)") case .success(let user): print("Login as \(user) succeeded!") // Continue below } // Set up the client, database, and collection. let client = self.appClient.currentUser!.mongoClient("mongodb-atlas") let database = client.database(named: "ios") let collection = database.collection(withName: "CoffeeDrinks") // Watch the collection. In this example, we use a queue and delegate, // both of which are optional arguments. // `filterIds` is an array of specific document ObjectIds you want to watch. let queue = DispatchQueue(label: "io.realm.watchQueue") let delegate = MyChangeStreamDelegate() let changeStream = collection.watch(filterIds: [drinkObjectId], delegate: delegate, queue: queue) // An update to a relevant document triggers a change event. let queryFilter: Document = ["_id": AnyBSON(drinkObjectId) ] let documentUpdate: Document = ["$set": ["containsDairy": true]] collection.updateOneDocument(filter: queryFilter, update: documentUpdate) { result in switch result { case .failure(let error): print("Call to MongoDB failed: \(error.localizedDescription)") return case .success(let updateResult): print("Successfully updated the document") } } // After you're done watching for events, close the change stream. changeStream.close() } }
Login as <RLMUser: 0x60000010eb00> succeeded! Successfully inserted a document with id: objectId(64e525ce7765243942ef0a58) Change stream opened: <RLMChangeStream: 0x6000034946c0> Change event document received: [ "fullDocument": Optional(RealmSwift.AnyBSON.document([ "containsDairy": Optional(RealmSwift.AnyBSON.bool(true)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia")), "_id": Optional(RealmSwift.AnyBSON.objectId(64e525ce7765243942ef0a58)), "name": Optional(RealmSwift.AnyBSON.string("Bean of the Day")), "storeNumber": Optional(RealmSwift.AnyBSON.int64(42)) ])), "clusterTime": Optional(RealmSwift.AnyBSON.datetime(2023-08-22 21:17:09 +0000)), "operationType": Optional(RealmSwift.AnyBSON.string("update")), "documentKey": Optional(RealmSwift.AnyBSON.document([ "_id": Optional(RealmSwift.AnyBSON.objectId(64e525ce7765243942ef0a58)) ])), "ns": Optional(RealmSwift.AnyBSON.document([ "db": Optional(RealmSwift.AnyBSON.string("ios")), "coll": Optional(RealmSwift.AnyBSON.string("CoffeeDrinks")) ])), "updateDescription": Optional(RealmSwift.AnyBSON.document([ "removedFields": Optional(RealmSwift.AnyBSON.array([])), "updatedFields": Optional(RealmSwift.AnyBSON.document([ "containsDairy": Optional(RealmSwift.AnyBSON.bool(true))])) ])), "_id": Optional(RealmSwift.AnyBSON.document([ "_data": Optional(RealmSwift.AnyBSON.string( "8264E525D5000000082B022C0100296E5A100464816C3449884434A07AC19F4AAFCB8046645F6964006464E525CE7765243942EF0A580004" )) ]))] Change stream closed
监视具有筛选器的集合中的更改
您可以通过调用collection.watch(matchFilter:)打开对集合中满足特定条件的文档所做的更改流。 此方法接受一个Document
参数,该参数用作$match 操作符的查询,以处理监视集合时发生的每个数据库事件。
class MyChangeStreamDelegate: ChangeEventDelegate { func changeStreamDidOpen(_ changeStream: RealmSwift.ChangeStream) { print("Change stream opened: \(changeStream)") } func changeStreamDidClose(with error: Error?) { if let anError = error { print("Change stream closed with error: \(anError.localizedDescription)") } else { print("Change stream closed") } } func changeStreamDidReceive(error: Error) { print("Received error: \(error.localizedDescription)") } func changeStreamDidReceive(changeEvent: RealmSwift.AnyBSON?) { guard let changeEvent = changeEvent else { return } guard let document = changeEvent.documentValue else { return } print("Change event document received: \(document)") } }
CoffeeDrink
以下代码使用此委托监视collection中的文档更改。它仅Atlas Triggers为其文档的storeNumber
值为42
的事件提供的回调:
appClient.login(credentials: Credentials.anonymous) { (result) in DispatchQueue.main.async { switch result { case .failure(let error): print("Login failed: \(error)") case .success(let user): print("Login as \(user) succeeded!") // Continue below } // Set up the client, database, and collection. let client = self.appClient.currentUser!.mongoClient("mongodb-atlas") let database = client.database(named: "ios") let collection = database.collection(withName: "CoffeeDrinks") // Watch the collection. In this example, we use a queue and delegate, // both of which are optional arguments. let queue = DispatchQueue(label: "io.realm.watchQueue") let delegate = MyChangeStreamDelegate() let matchFilter = [ "fullDocument.storeNumber": AnyBSON(42) ] let changeStream = collection.watch(matchFilter: matchFilter, delegate: delegate, queue: queue) // An update to a relevant document triggers a change event. let queryFilter: Document = ["_id": AnyBSON(drinkObjectId) ] let documentUpdate: Document = ["$set": ["containsDairy": true]] collection.updateOneDocument(filter: queryFilter, update: documentUpdate) { result in switch result { case .failure(let error): print("Call to MongoDB failed: \(error.localizedDescription)") return case .success(let updateResult): print("Successfully updated the document") } } // After you're done watching for events, close the change stream. changeStream.close() } }
Login as <RLMUser: 0x6000026ee640> succeeded! Successfully inserted a document with id: objectId(64e5266731323150716faf13) Change stream opened: <RLMChangeStream: 0x6000013283c0> Change event document received: [ "operationType": Optional(RealmSwift.AnyBSON.string("update")), "updateDescription": Optional(RealmSwift.AnyBSON.document([ "removedFields": Optional(RealmSwift.AnyBSON.array([])), "updatedFields": Optional(RealmSwift.AnyBSON.document([ "containsDairy": Optional(RealmSwift.AnyBSON.bool(true)) ])) ])), "clusterTime": Optional(RealmSwift.AnyBSON.datetime(2023-08-22 21:19:44 +0000)), "_id": Optional(RealmSwift.AnyBSON.document([ "_data": Optional(RealmSwift.AnyBSON.string( "8264E526700000000E2B022C0100296E5A100464816C3449884434A07AC19F4AAFCB8046645F6964006464E5266731323150716FAF130004" )) ])), "ns": Optional(RealmSwift.AnyBSON.document([ "db": Optional(RealmSwift.AnyBSON.string("ios")), "coll": Optional(RealmSwift.AnyBSON.string("CoffeeDrinks")) ])), "fullDocument": Optional(RealmSwift.AnyBSON.document([ "_id": Optional(RealmSwift.AnyBSON.objectId(64e5266731323150716faf13)), "name": Optional(RealmSwift.AnyBSON.string("Bean of the Day")), "containsDairy": Optional(RealmSwift.AnyBSON.bool(true)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia")), "storeNumber": Optional(RealmSwift.AnyBSON.int64(42)) ])), "documentKey": Optional(RealmSwift.AnyBSON.document([ "_id": Optional(RealmSwift.AnyBSON.objectId(64e5266731323150716faf13)) ]))] Successfully updated the document Change stream closed
以异步序列形式观看collection
版本 10.37.0 中的新增内容。
您可以打开异步序列来监视集合的更改。 在异步上下文中,对集合调用changeEvents()以打开变更流。 这提供了 AnyBSON 值的异步序列,其中包含有关 MongoDB 集合的每次更改的信息。
您可以选择提供changeEvents(onOpen: )回调,当监视流在服务器上初始化时会调用该回调。
与上面的示例类似, changeEvents()
API 可以使用filterIds
或matchFilter
来监视collection中的文档子集。
以下代码段将以异步序列的形式监视对CoffeeDrinks
集合中任何文档的更改:
let user = try await appClient.login(credentials: Credentials.anonymous) // Set up the client, database, and collection. let mongoClient = user.mongoClient("mongodb-atlas") let database = mongoClient.database(named: "ios") let collection = database.collection(withName: "CoffeeDrinks") // Set up a task you'll later await to keep the change stream open, // and you can cancel it when you're done watching for events. let task = Task { // Open the change stream. let changeEvents = collection.changeEvents(onOpen: { print("Successfully opened change stream") }) // Await events in the change stream. for try await event in changeEvents { let doc = event.documentValue! print("Received event: \(event.documentValue!)") } } // Updating a document in the collection triggers a change event. let queryFilter: Document = ["_id": AnyBSON(objectId) ] let documentUpdate: Document = ["$set": ["containsDairy": true]] let updateResult = try await collection.updateOneDocument(filter: queryFilter, update: documentUpdate) // Cancel the task when you're done watching the stream. task.cancel() _ = await task.result
Successfully opened change stream Received event: [ "operationType": Optional(RealmSwift.AnyBSON.string("update")), "documentKey": Optional(RealmSwift.AnyBSON.document([ "_id": Optional(RealmSwift.AnyBSON.objectId(64e526d9850b15debe83ff46)) ])), "ns": Optional(RealmSwift.AnyBSON.document([ "coll": Optional(RealmSwift.AnyBSON.string("CoffeeDrinks")), "db": Optional(RealmSwift.AnyBSON.string("ios")) ])), "clusterTime": Optional(RealmSwift.AnyBSON.datetime(2023-08-22 21:21:30 +0000)), "fullDocument": Optional(RealmSwift.AnyBSON.document([ "name": Optional(RealmSwift.AnyBSON.string("Bean of the Day")), "containsDairy": Optional(RealmSwift.AnyBSON.bool(true)), "storeNumber": Optional(RealmSwift.AnyBSON.int64(43)), "_id": Optional(RealmSwift.AnyBSON.objectId(64e526d9850b15debe83ff46)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia")) ])), "_id": Optional(RealmSwift.AnyBSON.document([ "_data": Optional(RealmSwift.AnyBSON.string( "8264E526DA000000092B022C0100296E5A100464816C3449884434A07AC19F4AAFCB8046645F6964006464E526D9850B15DEBE83FF460004" ) ) ])), "updateDescription": Optional(RealmSwift.AnyBSON.document([ "updatedFields": Optional(RealmSwift.AnyBSON.document([ "containsDairy": Optional(RealmSwift.AnyBSON.bool(true)) ])), "removedFields": Optional(RealmSwift.AnyBSON.array([])) ]))]
聚合文档
聚合操作会通过一系列名为聚合管道的数据聚合阶段来运行集合中的所有文档。聚合允许您过滤和转换文档、收集有关相关文档群组的摘要数据以及其他复杂的数据操作。
您可以使用collection.aggregate() 在集合上配置和运行聚合操作。
聚合操作接受聚合阶段列表作为输入,并返回一个结果,该结果解析为管道处理的文档集合,或一个Error
。
筛选文档
您可以使用$match阶段以标准 MongoDB查询语法筛选文档:
此$match
阶段会筛选文档,以仅包含storeNumber
字段的值等于42
的文档:
let pipeline: [Document] = [["$match": ["storeNumber": ["$eq": 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)") } } }
Successfully ran the aggregation: Coffee drink: [ "_id": Optional(RealmSwift.AnyBSON.objectId(64e53171313231507183e7c2)), "name": Optional(RealmSwift.AnyBSON.string("Bean of the Day")), "containsDairy": Optional(RealmSwift.AnyBSON.bool(false)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia")), "storeNumber": Optional(RealmSwift.AnyBSON.int64(42))] Coffee drink: [ "containsDairy": Optional(RealmSwift.AnyBSON.bool(true)), "name": Optional(RealmSwift.AnyBSON.string("Maple Latte")), "beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia")), "_id": Optional(RealmSwift.AnyBSON.objectId(64e53171313231507183e7c3)), "storeNumber": Optional(RealmSwift.AnyBSON.int64(42))] (...more results...)
对文档分组
您可以使用$group阶段聚合一个或多个文档的摘要数据。 MongoDB 根据$group
阶段的_id
字段中定义的表达式对文档进行分组。 您可以通过在字段名称前加上$
前缀来引用特定的文档字段。
此$group
阶段按文档的storeNumber
字段值对文档进行排列。 然后,它计算字段值中包含该商店数字的咖啡饮料文档的数量。 换句话说,我们正在计算每个商店编号的咖啡饮料数量。
let pipeline: [Document] = [["$group": ["_id": "$storeNumber", "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) } } }
Successfully ran the aggregation. ["numItems": Optional(RealmSwift.AnyBSON.int64(27)), "_id": Optional(RealmSwift.AnyBSON.int64(42))] ["numItems": Optional(RealmSwift.AnyBSON.int64(44)), "_id": Optional(RealmSwift.AnyBSON.int64(47))] (...more results...)
项目文档字段
您可以使用 $project 阶段包含或省略文档中的特定字段,或使用聚合运算符计算新字段。投影有两种工作方式:
使用
1
指定要包含的字段。这样做的不利影响是会隐式排除所有未指定的字段。使用
0
指定要排除的字段。这样做的副作用是会隐式包含所有未指定的字段。
这两种投影方法是互斥的。 如果指定了要包含的字段,则不能同时指定要排除的字段,反之亦然。
注意
_id
字段是一个特例:除非另行显式指定,否则它始终包含在每个查询中。因此,您可以排除具有 0
值的 _id
字段,同时包含其他字段,例如具有 1
的 storeNumber
。只有排除 _id
字段的特殊情况才允许在一个 $project
阶段中同时进行排除和包含。
对于本示例,假设CoffeeDrink
文档有一个store
字段,该字段是一个包含单词“Store”和数字的字符串值,例如“Store 42”,与以下文档类似:
let drink: Document = [ "name": "Bean of the Day", "beanRegion": "Timbio, Colombia", "containsDairy": false, "store": "Store 42"] let drink2: Document = [ "name": "Bean of the Day", "beanRegion": "Yirgacheffe, Ethiopia", "containsDairy": true, "store": "Store 47"]
以下 $project
阶段会省略 _id
字段,包含 name
字段,并创建一个名为 storeNumber
的新字段。storeNumber
是使用两个聚合运算符生成的:
$split
将store
字符串表示形式分成位于空格字符周围的两个字符串段。 例如,以这种方式分割值“Store 42”会返回一个包含两个元素的数组:“Store”和“42”。$arrayElemAt
会根据第二个参数从数组中选择特定元素。在本例中,1
值会从$split
运算符生成的数组中选择第二个元素,因为数组索引会从0
开始。例如,传递给此操作的值 ["Store", "42"] 将返回值“42”。
let pipeline: [Document] = [["$project": ["_id": 0, "name": 1, "storeNumber": ["$arrayElemAt": [["$split": ["$store", " "]], 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) } } }
Successfully ran the aggregation. ["name": Optional(RealmSwift.AnyBSON.string("Bean of the Day")), "storeNumber": Optional(RealmSwift.AnyBSON.string("42"))] ["storeNumber": Optional(RealmSwift.AnyBSON.string("47")), "name": Optional(RealmSwift.AnyBSON.string("Bean of the Day"))] (...more results...)
向文档添加字段
您可以在$addFields阶段通过聚合操作符添加具有计算值的新字段。
注意
$addFields
与 $project 类似,但不允许包含或省略字段。
对于本示例,假设CoffeeDrink
文档有一个store
字段,该字段是一个包含单词“Store”和数字的字符串值,例如“Store 42”,与以下文档类似:
let drink: Document = [ "name": "Bean of the Day", "beanRegion": "Timbio, Colombia", "containsDairy": false, "store": "Store 42"] let drink2: Document = [ "name": "Bean of the Day", "beanRegion": "Yirgacheffe, Ethiopia", "containsDairy": true, "store": "Store 47"]
以下 $addFields
阶段将创建一个名为 storeNumber
的新字段,其中该字段的值为用于转换 store
字段值的两个聚合运算符的输出。
let pipeline: [Document] = [["$addFields": ["storeNumber": ["$arrayElemAt": [["$split": ["$store", " "]], 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) } } }
Successfully ran the aggregation. [ "storeNumber": Optional(RealmSwift.AnyBSON.string("42")), "name": Optional(RealmSwift.AnyBSON.string("Bean of the Day")), "_id": Optional(RealmSwift.AnyBSON.objectId(64e588ff5fef1743de3559aa)), "store": Optional(RealmSwift.AnyBSON.string("Store 42")), "containsDairy": Optional(RealmSwift.AnyBSON.bool(false)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Timbio, Colombia"))] [ "containsDairy": Optional(RealmSwift.AnyBSON.bool(true)), "storeNumber": Optional(RealmSwift.AnyBSON.string("47")), "store": Optional(RealmSwift.AnyBSON.string("Store 47")), "beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia")), "name": Optional(RealmSwift.AnyBSON.string("Bean of the Day")), "_id": Optional(RealmSwift.AnyBSON.objectId(64e588ff5fef1743de3559ab))]
Unwind Array Values
您可以使用 $unwind 阶段将包含数组的单个文档转换为包含该数组中各个值的多个文档。当您展开数组字段时,MongoDB 会为该数组字段的每个元素复制每个文档一次,但同时还会在每个副本中用数组元素替换数组值。
考虑此包含featuredInPromotions
数组的文档:
let drink: Document = [ "name": "Maple Latte", "beanRegion": "Yirgacheffe, Ethiopia", "containsDairy": true, "storeNumber": 42, "featuredInPromotions": [ "Spring into Spring", "Tastes of Fall", "Winter Delights" ] ]
下面的 $unwind
阶段为每个文档中 items
数组的每个元素创建一个新文档。该阶段还会在每个新文档中添加一个名为 itemIndex
的字段,指定元素在原始数组中的位置索引:
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)") } } }
Successfully ran the aggregation. Coffee drink: [ "_id": Optional(RealmSwift.AnyBSON.objectId(64e58bb4fc901d40e03fde64)), "storeNumber": Optional(RealmSwift.AnyBSON.int64(42)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia")), "featuredInPromotions": Optional(RealmSwift.AnyBSON.string("Spring into Spring")), "itemIndex": Optional(RealmSwift.AnyBSON.int64(0)), "name": Optional(RealmSwift.AnyBSON.string("Maple Latte")), "containsDairy": Optional(RealmSwift.AnyBSON.bool(true))] Coffee drink: [ "itemIndex": Optional(RealmSwift.AnyBSON.int64(1)), "name": Optional(RealmSwift.AnyBSON.string("Maple Latte")), "_id": Optional(RealmSwift.AnyBSON.objectId(64e58bb4fc901d40e03fde64)), "featuredInPromotions": Optional(RealmSwift.AnyBSON.string("Tastes of Fall")), "storeNumber": Optional(RealmSwift.AnyBSON.int64(42)), "containsDairy": Optional(RealmSwift.AnyBSON.bool(true)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia"))] Coffee drink: [ "name": Optional(RealmSwift.AnyBSON.string("Maple Latte")), "_id": Optional(RealmSwift.AnyBSON.objectId(64e58bb4fc901d40e03fde64)), "beanRegion": Optional(RealmSwift.AnyBSON.string("Yirgacheffe, Ethiopia")), "itemIndex": Optional(RealmSwift.AnyBSON.int64(2)), "containsDairy": Optional(RealmSwift.AnyBSON.bool(true)), "storeNumber": Optional(RealmSwift.AnyBSON.int64(42)), "featuredInPromotions": Optional(RealmSwift.AnyBSON.string("Winter Delights"))]
然后,您可以按featuredInPromotions
的值和$sum
每次促销中的咖啡饮料数量进行$group
(如群组文档示例中所示),或者根据您的数据执行其他计算或转换。