管理 Flexible Sync 订阅 - Swift SDK
在此页面上
Overview
版本 10.22.0 新增内容。
Flexible Sync 使用订阅和权限来确定要与应用同步哪些数据。
在 iOS 客户端中使用“灵活同步”:
可以手动添加、更新和删除订阅,以确定将哪些数据同步到客户端设备。在 Realm Swift SDK 版本 10.43.0 及更高版本中,可以订阅查询,而不是手动管理订阅,或者除了手动管理订阅之外还可以订阅查询。
提示
Flexible Sync支持 组合。
订阅
当您在后端配置“灵活同步”时,可指定客户端应用程序可使用订阅查询哪些字段。
每个订阅对应于对特定对象类型的可查询字段的查询。 有关更多信息,请参阅 Atlas App Services文档中的可查询 字段 。
对于每个查询订阅,Realm 都会查找与查询匹配的数据。与订阅匹配的数据(其中用户具有适当的权限)在客户端和后端应用程序之间同步。
可以使用 Realm Swift SDK 查询引擎构建查询。
订阅对象类型
订阅集基于对象类型。如果您有多种类型的 Realm 对象,则可能有多个订阅。您还可以对同一对象类型拥有多个订阅。
但是,如果您在应用程序中使用关系或不对称对象,请注意以下因素:
对象链接
要查看链接对象,必须将对象及其链接对象都添加到订阅集中。
如果订阅结果包含一个对象,该对象的属性链接到结果中未包含的对象,则该链接显示为空。我们无法区分该属性的值是否为合法零值,或者所链接的对象是否存在但不在查询订阅的视图中。
不对称对象
如果应用程序使用数据导入单向同步不对称对象,则无法为这些对象创建订阅。如果应用程序包含同一域中的不对称对象和非不对称对象,则您可以为非不对称对象添加“Flexible Sync”订阅查询。
权限
订阅与权限协同工作,以确定要同步到客户端应用程序的数据。客户端应用程序只能看到与订阅匹配的数据子集,该子集也与登录用户的权限匹配。
本页详细介绍如何管理 Flexible Sync 的客户端订阅。 有关设置Flexible Sync权限的信息,请参阅: Flexible Sync规则和权限。
在客户端应用程序中管理订阅
在客户端应用程序中,您可以添加、更新和删除对可查询字段的特定查询的订阅。这决定哪些数据会同步到客户端设备。
您可以:
添加具有可选订阅名称的订阅:
在 Realm Swift SDK 版本 10.43.0 及更高版本中,可以使用
.subscribe()
订阅查询Results
。这会自动将订阅添加到订阅集。使用
subscriptions
API 手动将订阅添加到订阅集。如果出于性能优化或业务逻辑原因需要对订阅进行更多控制,请使用此 API。有关更多信息,请参阅性能注意事项。
响应订阅状态
使用新查询更新订阅
删除对象类型的单个订阅或所有订阅
关于本页中的示例
本页中的示例使用任务列表应用程序的简单数据集。两种 Realm 对象类型分别是 Team
和 Task
。Task
具有 taskName
、受让人名称和已完成标志。此外,还有工作分钟计数和到期日期。Team
有一个 teamName
,零个或多个 Tasks
以及 members
列表。
class Task: Object { true) var _id: ObjectId (primaryKey: var taskName: String var assignee: String? var completed: Bool var progressMinutes: Int var dueDate: Date } class Team: Object { true) var _id: ObjectId (primaryKey: var teamName: String var tasks: List<Task> var members: List<String> }
本页面上的示例还假设您拥有授权用户和灵活同步配置:
let app = App(id: APPID) do { let credentials = emailPasswordCredentials(app: app) let user = try await app.login(credentials: credentials) var flexSyncConfig = user.flexibleSyncConfiguration() flexSyncConfig.objectTypes = [Task.self, Team.self] do { // Open the synced realm and manage Flexible Sync subscriptions } catch { print("Failed to open realm: \(error.localizedDescription)") // handle error } } catch { fatalError("Login failed: \(error.localizedDescription)") }
订阅查询
10.43.0 版新增内容。
为了简化订阅管理,Realm Swift SDK 版本 10.43.0 添加了 API 来订阅和取消订阅查询的 Results
集。这些 API 采用抽象方式表示手动添加和删除订阅的详细信息。
重要
.subcribe() API 处于预览状态
此处描述的 .subscribe()
和 .unsubscribe()
API 目前为预览状态。这些 API 将来可能会发生变化。
订阅查询
使用经过身份验证的用户和 Flexible Sync 配置,您可以打开同步 Realm 并查询要读取和写入的对象。 您可以.subscribe() 该查询,为与该查询匹配的对象创建 Flexible Sync 订阅:
let realm = try await Realm(configuration: flexSyncConfig) let results = try await realm.objects(Task.self) .where { $0.progressMinutes >= 60 }.subscribe() // Go on to work with subscribed results
这将创建一个未命名的订阅并将其添加到MutableSubscriptionSet
中,类似于手动创建订阅。
使用订阅名称订阅查询
如果应用程序支持多个订阅,或者想要更新一个订阅,则可能需要在订阅查询时添加名称。
您稍后可以使用此名称来更新订阅的查询、按名称检查订阅或删除按名称查询。
let realm = try await Realm(configuration: flexSyncConfig) let results = try await realm.objects(Team.self) .where { $0.teamName == "Developer Education" } .subscribe(name: "team_developer_education") // Go on to work with subscribed results
等待查询订阅同步
订阅查询的 Results
集时,该集在同步之前不包含对象。如果应用创建对象,则可能不需要在用户使用同步数据之前进行下载。但是,如果应用在用户可以使用之前需要来自服务器的数据,则可以指定订阅应 waitForSync
:
let realm = try await Realm(configuration: flexSyncConfig) let results = try await realm.objects(Team.self) .where { $0.members.contains("Bob Smith") } .subscribe( name: "bob_smith_teams", waitForSync: .onCreation) // After waiting for sync, the results set contains all the objects // that match the query - in our case, 1 print("The number of teams that have Bob Smith as a member is \(results.count)")
此选项使用 RLMWaitForSyncMode
枚举,其情况如下:
.onCreation:应用程序创建订阅时等待下载匹配对象。否则,无需等待新下载即可返回。第一次添加订阅时,应用程序必须具有互联网连接。
.always:执行
.subscribe()
时等待下载匹配对象。执行.subscribe()
时,应用程序必须具有互联网连接。.never:永远不要等待下载匹配对象。该应用需要互联网连接才能让用户在首次启动应用时进行身份验证,但可以在后续启动时使用缓存的凭据离线打开。
您可以选择指定timeout
TimeInterval 类型的 值。
取消订阅查询
可以使用 .unsubscribe() API 取消订阅查询的 Results
集:
let realm = try await Realm(configuration: flexSyncConfig) let results = try await realm.objects(Task.self).where { $0.completed == false }.subscribe() // Go on to work with subscribed results. // Later... results.unsubscribe()
这将从MutableSubscriptionSet
中删除订阅,类似于手动删除订阅。
如果存在另一个包含重叠对象的订阅,则在调用 .unsubscribe()
后,Results
集可能仍包含对象。
调用 .unsubscribe()
不会等待对象从 Realm 中删除。没有 API 可以等待 .unsubscribe()
与服务器同步。
订阅执行者限制的查询
您可以在 MainActor 上订阅受 actor 限制的查询:
let realm = try await Realm(configuration: flexSyncConfig, actor: MainActor.shared) let results = try await realm.objects(Team.self) .where { $0.teamName == "Developer Education" } .subscribe(name: "team_developer_education") // Go on to work with subscribed results
或者订阅有关自定义 actor 的查询:
let realm = try await Realm(configuration: flexSyncConfig, actor: CustomGlobalActor.shared) let results = try await realm.objects(Team.self) .where { $0.teamName == "Developer Education" } .subscribe(name: "team_developer_education") // Go on to work with subscribed results
有关受 actor 限制的 Realm 的更多信息,请参阅将 Realm 与 Actor 结合使用 - Swift SDK。
手动管理订阅
可以使用 subscriptions
API 手动管理对可查询字段的特定查询进行的一组订阅。
您可以:
添加订阅
响应订阅状态
使用新查询更新订阅
删除对象类型的单个订阅或所有订阅
与订阅匹配的数据(其中用户具有适当的权限)在设备和后端应用程序之间同步。
您可以为订阅指定一个可选的字符串名称。
创建订阅时,Realm 会查找与特定对象类型的查询匹配的数据。您可以在不同的对象类型上拥有多个订阅集。您还可以对同一对象类型进行多次查询。
例子
您可以使用显式名称创建订阅。然后,您可以按名称搜索该订阅,执行更新或删除操作。
QuerySubscription<Task>(name: "long-running-completed") { $0.completed == true && $0.progressMinutes > 120 }
如果没有为订阅指定 name
,则可通过查询字符串搜索订阅。
QuerySubscription<Team> { $0.teamName == "Developer Education" }
注意
重复订阅
订阅名称必须是唯一名称。尝试附加与现有订阅名称相同的订阅时,会引发错误。
如果您没有显式命名订阅,而是多次订阅相同的未命名查询,则 Realm 不会将重复查询保存到订阅集。
如果您以不同的名称多次订阅同一查询,则 Realm 会将这两个订阅保存到订阅集。
添加订阅
在订阅更新区块中添加订阅。将每个新订阅附加到客户端的 Realm 订阅中。
提示
如果您的应用会在 async/await
上下文中访问 Realm,请使用 @MainActor
来标记此代码,从而避免出现与线程相关的崩溃。
let realm = try await getRealmWithSingleSubscription() // Opening a realm and accessing it must be done from the same thread. // Marking this function as `@MainActor` avoids threading-related issues. func getRealmWithSingleSubscription() async throws -> Realm { let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.append( QuerySubscription<Team> { $0.teamName == "Developer Education" }) } return realm }
您可以在一个订阅更新区块中添加多个订阅,包括不同对象类型的订阅。
let realm = try await getRealmWithMultipleSubscriptions() // Opening a realm and accessing it must be done from the same thread. // Marking this function as `@MainActor` avoids threading-related issues. func getRealmWithMultipleSubscriptions() async throws -> Realm { let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.append( QuerySubscription<Task>(name: "completed-tasks") { $0.completed == true }) subscriptions.append( QuerySubscription<Team> { $0.teamName == "Developer Education" }) } return realm }
通过初始订阅引导 Realm
版本 10.28.0 新增内容。
您必须至少有一个订阅,才能读取或写入 Realm。 当您使用FlexibleSyncConfiguration()打开 Realm 时,可以使用初始订阅集来引导 Realm。 将initialSubscriptions
参数与要用于引导 Realm 的订阅查询一起传递:
var flexSyncConfig = user.flexibleSyncConfiguration(initialSubscriptions: { subs in subs.append( QuerySubscription<Team> { $0.teamName == "Developer Education" }) })
如果应用程序需要在每次启动时重新运行此初始订阅,可以传递一个额外的参数 rerunOnOpen
。这是一个布尔值,表示初始订阅是否应在每次应用程序启动时重新运行。可能需要执行此操作以重新运行动态时间范围或其他需要重新计算订阅的静态变量的查询。
在此示例中,我们不希望用户被不相关的任务淹没,因此我们将仅加载前 7 天内和后 7 天内到期的任务。一周前到期的任务不再相关,下周之后到期的任务也不再相关。通过 rerunOnOpen
,每次启动应用时,查询都会根据所需的日期范围动态地重新计算要同步的相关对象。
// Set the date a week ago and the date a week from now, as those are the dates we'll use // in the Flexible Sync query. `rerunOnOpen` lets the app recalculate this query every // time the app opens. let secondsInAWeek: TimeInterval = 604800 let dateLastWeek = (Date.now - secondsInAWeek) let dateNextWeek = (Date.now + secondsInAWeek) var flexSyncConfig = user.flexibleSyncConfiguration(initialSubscriptions: { subs in subs.append( QuerySubscription<Task> { $0.dueDate > dateLastWeek && $0.dueDate < dateNextWeek }) }, rerunOnOpen: true)
订阅特定类型的所有对象
除了同步与给定查询匹配的所有对象外,还可以订阅特定类型的所有对象。可以通过附加订阅而不提供查询来完成此操作。
例如,如果不想查看特定团队,而是希望订阅所有 Team
对象,则可以执行以下操作:
let realm = try await subscribeToObjectsOfAType() // Opening a realm and accessing it must be done from the same thread. // Marking this function as `@MainActor` avoids threading-related issues. func subscribeToObjectsOfAType() async throws -> Realm { let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.append(QuerySubscription<Team>(name: "all_teams")) } XCTAssertEqual(subscriptions.count, 1) // :remove return realm }
添加订阅前检查现有订阅
如果应用程序流程在每次运行应用程序时将相同名称的订阅附加到订阅集,则不允许这样做。在这种情况下,在附加现有订阅之前添加对现有订阅的检查:
let realm = try await checkAndAddSubscription() // Opening a realm and accessing it must be done from the same thread. // Marking this function as `@MainActor` avoids threading-related issues. func checkAndAddSubscription() async throws -> Realm { let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions let foundSubscription = subscriptions.first(named: "user_team") try await subscriptions.update { if foundSubscription != nil { foundSubscription!.updateQuery(toType: Team.self, where: { $0.teamName == "Developer Education" }) } else { subscriptions.append( QuerySubscription<Team>(name: "user_team") { $0.teamName == "Developer Education" }) } } return realm }
等待订阅更改同步
本地更新订阅集只是更改订阅的组成部分。本地订阅更改后,Realm 与服务器同步,以解决由于订阅更改导致的任何数据更新。这可能意味着需要从同步 Realm 添加或删除数据。
异步前/等待
如果应用程序不使用 Swift 的异步/等待功能,则可以使用 onComplete
区块对与服务器同步的订阅更改做出反应。在订阅与服务器同步后调用此区块。例如,如果想通过重绘 UI 或根据数据集更改执行其他操作来对订阅状态更改做出反应,请在 onComplete
中执行这些操作。还可以在这里处理同步过程中出现的可选错误。
let subscriptions = realm.subscriptions subscriptions.update({ subscriptions.append( QuerySubscription<Task> { $0.assignee == "John Doe" }) }, onComplete: { error in // error is optional if error == nil { // Flexible Sync has updated data to match the subscription } else { // Handle the error } })
异步/等待
如果应用程序使用异步/等待功能,则不需要 onComplete
区块。更新以异步方式执行,如果更新无法成功完成,则会引发错误。
func changeSubscription() async throws { let subscriptions = realm.subscriptions try await subcriptions.update { subscriptions.remove { QuerySubscription<Task> { $0.assignee == "Joe Doe" } } } }
提示
如果您的应用会在 async/await
上下文中访问 Realm,请使用 @MainActor
来标记此代码,从而避免出现与线程相关的崩溃。
订阅设置状态
使用 SubscriptionSet.state 属性读取订阅集的当前状态。
superseded
状态是一种SyncSubscriptionState ,当另一个线程在订阅集的不同实例上更新订阅时,可能会出现该状态。 如果状态变为superseded
,则必须先获取订阅集的新实例,然后才能进行更新。
注意
订阅状态 Complete(完成)
订阅集状态“完成”并不意味着“同步已完成”或“所有文档已同步”。“完成”意味着发生了以下两件事:
该订阅已成为当前正在与服务器同步的活动订阅集。
在将订阅发送到服务器时与订阅匹配的文档现在位于本地设备上。请注意,这并不一定包括当前与订阅匹配的所有文档。
对于所有与订阅匹配的文档是否已同步到设备,Realm SDK 不提供检查方法。
使用新查询更新订阅
可以使用 updateQuery
更新订阅的查询。在此示例中,我们搜索与查询匹配的订阅,然后使用新查询进行更新。
let realm = try await getRealmWithUpdatedSubscriptions() // Opening a realm and accessing it must be done from the same thread. // Marking this function as `@MainActor` avoids threading-related issues. func getRealmWithUpdatedSubscriptions() async throws -> Realm { let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions try await subscriptions.update { if let foundSubscription = subscriptions.first(ofType: Team.self, where: { $0.teamName == "Developer Education" }) { foundSubscription.updateQuery(toType: Team.self, where: { $0.teamName == "Documentation" }) } } return realm }
您还可以按名称搜索订阅。在此示例中,我们按名称搜索订阅查询,然后使用新查询对其进行更新。
let realm = try await getRealmWithUpdatedSubscriptionName() // Opening a realm and accessing it must be done from the same thread. // Marking this function as `@MainActor` avoids threading-related issues. func getRealmWithUpdatedSubscriptionName() async throws -> Realm { let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions let foundSubscription = subscriptions.first(named: "user-team") try await subscriptions.update { foundSubscription?.updateQuery(toType: Team.self, where: { $0.teamName == "Documentation" }) } return realm }
删除订阅
要删除订阅,您可以:
删除单个订阅查询
删除特定对象类型的所有订阅
删除所有未命名的订阅
删除所有订阅
当您删除订阅查询时,Realm 会异步删除与客户端设备中的查询匹配的同步数据。
删除单个订阅
可以在订阅更新区块中使用 remove
删除特定的订阅查询。按名称指定查询或将查询用作字符串,以查找要删除的相应订阅查询。
let realm = try await getRealmAfterRemovingSubscription() // Opening a realm and accessing it must be done from the same thread. // Marking this function as `@MainActor` avoids threading-related issues. func getRealmAfterRemovingSubscription() async throws -> Realm { let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions // Look for a specific subscription, and then remove it let foundSubscription = subscriptions.first(named: "docs-team") try await subscriptions.update { subscriptions.remove(foundSubscription!) } // Or remove a subscription that you know exists without querying for it try await subscriptions.update { subscriptions.remove(named: "existing-subscription") } return realm }
删除对象类型的所有订阅
要删除对特定对象类型的所有订阅,请在订阅更新区块中结合使用 removeAll
方法和 ofType
。
let realm = try await getRealmAfterRemovingAllSubscriptionsToAnObjectType() // Opening a realm and accessing it must be done from the same thread. // Marking this function as `@MainActor` avoids threading-related issues. func getRealmAfterRemovingAllSubscriptionsToAnObjectType() async throws -> Realm { let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.removeAll(ofType: Team.self) } return realm }
删除所有未命名的订阅
10.43.0 版新增内容。
您可能想要删除临时或动态生成的未命名订阅,但保留已命名订阅。
调用 removeAll
方法时,可以通过将 unnamedOnly
设置为 true
从订阅集中删除所有未命名的订阅:
let realm = try await Realm(configuration: flexSyncConfig) // Add 2 subscriptions, one named and one unnamed. let results = try await realm.objects(Team.self).where { $0.teamName == "Developer Education" }.subscribe(name: "team_developer_education") let results2 = try await realm.objects(Task.self).where { $0.completed == false }.subscribe() // Later, remove only the unnamed one let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.removeAll(unnamedOnly: true) }
删除所有订阅
要从订阅集中删除所有订阅,请在订阅更新区块中使用 removeAll
方法。
重要
如果删除所有订阅且未添加新订阅,则会出现错误。使用 Flexible Sync 配置打开的 Realm 至少需要一个订阅才能与服务器同步。
let realm = try await getRealmAfterRemovingAllSubscriptions() // Opening a realm and accessing it must be done from the same thread. // Marking this function as `@MainActor` avoids threading-related issues. func getRealmAfterRemovingAllSubscriptions() async throws -> Realm { let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.removeAll() } return realm }
性能考虑因素
API 效率
手动管理订阅时,使用“订阅查询”部分中描述的.subscribe()
和.unsubscribe()
API 添加多个订阅比执行批量更新效率低。 每次.subscribe()
时,Swift SDK 都会打开一个新的更新区块。 为了提高添加多个订阅的性能,请使用“手动管理订阅”部分中描述的subscriptions.update
API。
更新群组以提高性能
订阅集的每个写入事务都会产生性能成本。如果需要在会话期间对 Realm 对象进行多次更新,请考虑将编辑的对象保留在内存中,直到所有更改完成。这通过仅将完整且更新的对象写入 Realm 而不是每次更改来提高同步性能。
“灵活同步”RQL 的要求和限制
已索引可查询字段订阅的要求
向应用添加索引可查询字段可以提高对强分区数据进行简单查询的性能。 例如,如果查询将数据强映射到设备、商店或用户的应用(例如user_id == $0, “641374b03725038381d2e1fb”
,则非常适合使用索引可查询字段。 但是,在查询订阅中使用索引化可查询字段有特定的要求:
每个订阅查询中都必须使用索引化可查询字段。查询中不能缺少该字段。
在订阅查询中,索引化可查询字段必须使用
==
或IN
进行至少一次针对常量的比较。例如,user_id == $0, "641374b03725038381d2e1fb"
或store_id IN $0, {1,2,3}
。
可以选择包含 AND
比较,前提是使用 ==
或 IN
将索引化可查询字段直接与常量进行至少一次比较。例如,store_id IN {1,2,3} AND region=="Northeast"
或 store_id == 1 AND (active_promotions < 5 OR num_employees < 10)
。
对索引化可查询字段的无效灵活同步查询包括以下情况的查询:
索引化可查询字段未将
AND
与查询的其余部分结合使用。例如,store_id IN {1,2,3} OR region=="Northeast"
是无效的,因为它使用了OR
而不是AND
。同样,store_id == 1 AND active_promotions < 5 OR num_employees < 10
也是无效的,因为AND
仅适用于其旁边的词,而不适用于整个查询。索引化可查询字段未在相等运算符中使用。例如,
store_id > 2 AND region=="Northeast"
是无效的,因为它仅将>
运算符与索引化可查询字段结合使用,而没有相等比较。查询中完全没有索引化可查询字段。例如,
region=="Northeast
和truepredicate
都是无效的,因为它们不含索引化可查询字段。
灵活同步中不支持的查询运算符
使用 RQL 操作符时,Flexible Sync有一些限制。当您编写确定要同步哪些数据的查询订阅时,服务器不支持这些查询操作符。但是,您仍然可以使用全部的 RQL 功能来查询客户端应用中的同步数据集。
运算符类型 | 不支持的运算符 |
---|---|
聚合操作符 |
|
查询后缀 |
|
不区分大小写的查询 ([c]
) 无法有效地使用索引。因此,不建议使用不区分大小写的查询,因为它们可能会导致性能问题。
灵活同步仅支持数组字段的 @count
。
列出查询
灵活同步支持使用 IN
运算符来查询列表。
可以查询常量列表,查看其中是否包含可查询字段的值:
// Query a constant list for a queryable field value "priority IN { 1, 2, 3 }"
如果某个可查询字段具有数组值,则可以通过查询确定其中是否包含常量值:
// Query an array-valued queryable field for a constant value "'comedy' IN genres"
警告
您无法在灵活同步查询中相互比较两个列表。请注意,这是灵活同步查询之外的有效 Realm 查询语言语法。
// Invalid Flexible Sync query. Do not do this! "{'comedy', 'horror', 'suspense'} IN genres" // Another invalid Flexible Sync query. Do not do this! "ANY {'comedy', 'horror', 'suspense'} != ANY genres"
嵌入式对象或关联对象
灵活同步不支持查询嵌入式对象或链接中的属性。例如,obj1.field == "foo"
。
查询大小限制
订阅设立任何给定查询订阅的大小限制为256 kB 。 超过此限制会导致LimitsExceeded 错误。