Overview
使用Flexible Sync将数据写入同步 Realm 时,您可以使用与写入本地 Realm 相同的 API。 但是,在开发应用程序时,需要记住一些行为差异。
当您写入同步 Realm 时,写入操作必须匹配以下两项:
- 同步订阅查询。
如果写入操作与订阅中的查询不匹配,则写入将恢复并显示非致命补偿写入错误 (ErrorCompensatingWrite)。
- App Services App 中的权限。
如果尝试写入与权限表达式不匹配的数据,写入操作将恢复为非致命权限被拒绝错误。 在客户端中,这显示为错误 (ErrorCompensatingWrite)。 在服务器上,您可以查看有关角色中的写入筛选器如何拒绝写入的更多详细信息。
要学习;了解有关为应用配置权限的更多信息,请参阅App Services文档中的基于角色的权限和Device Sync权限指南。
确定同步哪些数据
您可以写入同步域的数据是您的 Device Sync 配置、您的权限以及您在打开该域时使用的 Flexible Sync 订阅查询的交集。
本页上的示例使用以下配置和模型:
App Services 配置
Device Sync 配置有以下可查询字段:
_id(始终包含在内)complexityownerId
App Services App 的权限配置为仅允许用户读取和写入自己的数据:
{ "name": "owner-read-write", "apply_when": {}, "document_filters": { "read": { "ownerId": "%%user.id" }, "write": { "ownerId": "%%user.id" } }, "read": true, "write": true }
客户端Realm 数据模型和配置
本页上的示例使用以下对象模型:
class Item: Object { (primaryKey: true) var _id: ObjectId var ownerId: String var itemName: String var complexity: Int }
使用该 Realm 对象模型,同步域配置会同步与查询匹配的对象,其中complexity属性的值小于或等于4 :
let app = App(id: YOUR_APP_ID_HERE) do { let user = try await app.login(credentials: Credentials.anonymous) do { var flexSyncConfig = user.flexibleSyncConfiguration() flexSyncConfig.objectTypes = [Item.self] let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.append( QuerySubscription<Item>(name: "simple-items") { $0.complexity <= 4 }) } print("Successfully opened realm: \(realm)") } catch { print("Failed to open realm: \(error.localizedDescription)") // handle error } } catch { fatalError("Login failed: \(error.localizedDescription)") }
同步哪些数据?
订阅查询与权限相结合平均值着同步域仅在以下情况下同步对象:
ownerIduser.id与登录用户的 匹配(来自权限)complexity属性的值小于或等于4(来自订阅查询)
在Atlas collection中的任何对象,如果ownerId与登录用户的user.id不匹配,或者complexity属性的值大于4,则无法同步到此realm。
写入同步 Realm
对 Flexible Sync Realm 的写入大致可分为以下两类之一:
成功写入:写入的对象同时与查询订阅和用户权限匹配。 该对象成功写入到域,并成功同步到 App Services 后端和其他设备。
补偿写入:当写入的对象与订阅查询不匹配,或者用户没有足够的权限来执行写入时,Realm 将恢复非法写入。
成功写入
当写入与客户端中的权限和Flexible Sync订阅查询匹配时,Realm Swift SDK 可以成功将该对象写入同步 Realm。 当设备具有网络连接时,此对象会与Atlas App Services后端同步。
// This write falls within the subscription query and complies // with the Device Sync permissions, so this write should succeed. do { let learnRealm = Item() learnRealm.ownerId = user.id learnRealm.itemName = "Learn Realm CRUD stuff" learnRealm.complexity = 3 try realm.write { realm.add(learnRealm) } } catch { print("Failed to write to realm: \(error.localizedDescription)") }
补偿写入
在某些情况下,最初看似成功的写入实际上是非法写入。 在这些情况下,对象会写入数据库,但当数据库同步到后端时,Realm 会在称为补偿写入的非致命错误操作中恢复写入。 在以下情况下可能会进行补偿写入:
写入与查询订阅不匹配:写入的对象与用户的权限匹配,但与查询订阅不匹配。
写入与权限不匹配:写入的对象与查询订阅匹配,但与用户的权限不匹配。
更详细地说,当写入超出查询订阅范围或与用户权限不匹配的数据时,会出现以下情况:
由于客户端 Realm 没有“非法”写入的概念,因此写入最初会成功,直到 Realm 使用 App Services 后端解析变更集。
同步后,服务器会应用规则和权限。 服务器确定用户无权执行写入操作。
服务器向客户端发回恢复操作(称为“补偿写入”)。
客户端 Realm 恢复非法写入操作。
在对给定对象的非法写入和相应的补偿写入之间,客户端对该对象的任何写入都将丢失。
实际上,这可能看起来像一个对象被写入域,然后在服务器将补偿写操作发送回客户端后消失。
要了解有关权限被拒绝错误、补偿写入错误和其他Device Sync 错误类型的更多信息,请参阅 Atlas App Services文档中的 同步错误 。
Atlas App Services日志包含有关出现补偿写入错误的原因的更多信息。
补偿写入错误信息
版本 10.37.0 中的新增内容。
您可以在客户端中获取有关发生补偿写入原因的更多信息。 Swift SDK 在代码为.writeRejected的 SyncError 上公开compensatingWriteInfo字段。 您可以通过同步错误处理程序访问此信息。
该字段包含一个RLMCompensatingWriteInfo对象数组,其中提供:
客户端尝试写入的对象的
objectType特定对象的
primaryKey用于补偿写入错误的
reason
此信息与您在 App Services 日志中找到的信息相同。 它在客户端上公开是为了方便和调试。
例子
此错误处理程序举例说明了如何记录有关补偿写入错误的信息:
myApp.syncManager.errorHandler = { syncError, session in if let thisError = syncError as? SyncError { switch thisError.code { // ... additional SyncError.code cases ... case .writeRejected: if let compensatingWriteErrorInfo = thisError.compensatingWriteInfo { for anError in compensatingWriteErrorInfo { print("A write was rejected with a compensating write error") print("The write to object type: \(anError.objectType)") print("With primary key of: \(anError.primaryKey)") print("Was rejected because: \(anError.reason)") } } } } }
当发生补偿写入错误时,上面的示例错误处理程序会生成以下输出:
A write was rejected with a compensating write error The write to object type: Optional("Item") With primary key of: objectId(641382026852d9220b2e2bbf) Was rejected because: Optional("write to \"641382026852d9220b2e2bbf\" in table \"Item\" not allowed; object is outside of the current query view")
Optional("Item")此消息中的 是ItemSwift 模板应用中使用的 对象。主键是客户端尝试写入的特定对象的
objectId。table \"Item"\指的是此对象将同步的 Atlas collection。此示例中的
object is outside of the current query view原因是查询订阅设置为要求对象的isComplete属性为true,而客户端尝试写入此属性为false的对象。
与查询订阅不匹配的写入
只有与订阅查询匹配的对象,才能将其写入 Flexible Sync 域。如果您执行的写入与订阅查询不匹配,Realm 首先会写入对象,然后执行补偿写入。 这是一个非致命操作,可恢复与订阅查询不匹配的非法写入。
实际上,这可能看起来像写入成功,但当 Realm 与 App Services 后端同步并执行补偿写入时,该对象“消失”。
如果要写入与查询订阅不匹配的对象,则必须打开该对象与查询订阅匹配的另一个域。或者,您可以将对象写入不强制执行权限或订阅查询的非同步 域 。
代码示例
根据上述 Flexible Sync 域的配置,尝试写入此对象与查询订阅不匹配:
do { let fixTheBug = Item() fixTheBug.ownerId = user.id fixTheBug.itemName = "Fix the bug with the failing method" // The complexity of this item is `7`. This is outside the bounds // of the subscription query, so this write triggers a compensating write. fixTheBug.complexity = 7 try realm.write { realm.add(fixTheBug) } } catch { print("Failed to write to realm: \(error.localizedDescription)") }
客户端错误
在这种情况下,客户端日志中的错误消息为:
Sync: Connection[1]: Session[1]: Received: ERROR "Client attempted a write that is outside of permissions or query filters; it has been reverted" (error_code=231, try_again=true, error_action=Warning)
App Services 错误
在这种情况下,App Services 日志中的错误消息为:
"FlexibleSync_Item": { "63bdfc40f16be7b1e8c7e4b7": "write to \"63bdfc40f16be7b1e8c7e4b7\" in table \"FlexibleSync_Item\" not allowed; object is outside of the current query view" }
与权限不匹配的写入
当对象与用户的服务器端写入权限不匹配时,尝试写入客户端也可能会trigger补偿写入错误。
在客户端上,此类写入的行为与不匹配查询订阅的写入行为相同。 实际上,这可能看起来像写入成功,但当 Realm 与 App Services 后端同步并执行补偿写入时,该对象“消失”。
代码示例
鉴于上述 Device Sync 配置中的权限,尝试写入ownerId属性与登录用户的user.id不匹配的对象不是合法写入:
do { let itemWithWrongOwner = Item() // The `ownerId` of this item does not match the `user.id` of the logged-in // user. The user does not have permissions to make this write, so // it triggers a compensating write. itemWithWrongOwner.ownerId = "This string does not match the user.id" itemWithWrongOwner.itemName = "Write code that generates a permission error" itemWithWrongOwner.complexity = 1 try realm.write { realm.add(itemWithWrongOwner) } } catch { print("Failed to write to realm: \(error.localizedDescription)") }
客户端错误
这种情况下的客户端错误与您尝试写入查询筛选器之外的对象时的客户端错误相同:
Sync: Connection[1]: Session[1]: Received: ERROR "Client attempted a write that is outside of permissions or query filters; it has been reverted" (error_code=231, try_again=true, error_action=Warning)
App Services 错误
App Services 日志中的错误消息提供了一些附加信息,可帮助你确定这是权限问题,而不是查询订阅问题。 在此示例中,错误消息显示对象与用户角色不匹配:
"FlexibleSync_Item": { "63bdfc40f16be7b1e8c7e4b8": "write to \"63bdfc40f16be7b1e8c7e4b8\" in table \"FlexibleSync_Item\" was denied by write filter in role \"owner-read-write\"" }
对写入进行分组以提高性能
订阅集的每个写入事务都会产生性能成本。如果需要在会话期间对 Realm 对象进行多次更新,请考虑将编辑的对象保留在内存中,直到所有更改完成。这通过仅将完整且更新的对象写入 Realm 而不是每次更改来提高同步性能。
不要写入应用扩展中的同步 Realm
如果您正在开发使用应用扩展(例如共享扩展)的应用,请避免写入该扩展中的同步域。Device Sync 最多支持在一个进程中打开同步 Realm。 实际上,这意味着如果您的应用在 App Extensions 中使用同步域,则可能会间歇性崩溃。
与在多个进程中打开同步 Realm 相关的崩溃
如果您尝试在共享扩展或其他应用扩展中打开同步 Realm,并且该 Realm 未在主应用程序中打开,则从共享扩展写入可能会成功。 但是,如果同步域已在主应用程序中打开,或者正在背景同步数据,则可能会遇到与Realm::MultiSyncAgents相关的崩溃。在这种情况下,您可能需要重新启动设备。
在应用程序扩展中写入同步 Realm 的替代方案
如果您需要从 App Extensions 中读取或写入同步域,有一些推荐的替代方案:
离线优先:将磁盘上的数据传递给主应用程序或从主应用程序中传递出数据
始终保持最新:通过网络连接直接与后端 Atlas collection 通信
在磁盘上传递数据
如果离线优先功能是应用程序最重要的考虑因素,则可以将磁盘上的数据传递到主应用程序或从主应用程序中传递数据。 您可以将对象复制到非同步域,然后在应用组中的应用之间读取和共享这些对象。或者,您可以使用磁盘上队列向主应用程序发送数据或从主应用程序接收数据,并且仅从主应用程序写入同步域。这样,无论设备的网络连接如何,都可以随时与 App Extensions 之间共享信息。
直接与后端 Atlas collection通信
如果在所有设备上保持最新信息是您的应用最重要的考虑因素,则您可以通过网络直接向后端 Atlas collection 读取或写入数据。根据您的需求,您可能需要使用以下工具之一直接与 Atlas 通信:
使用Realm Swift SDK MongoClient查询 Atlas
将数据传递给App Services Function
使用Atlas Data API进行 HTTPS 调用
这样,任何具有网络连接的设备都可以始终获取最新信息,而无需像上面的选项一样等待用户打开主应用程序。
在使用应用扩展时,此选项确实要求用户的设备具有网络连接。 作为后备措施,您可以检查网络连接。 然后,在用户设备缺少网络连接的事件下,使用上述磁盘上选项。