手动客户端重置数据恢复 - Node.js SDK
本页介绍如何在客户端重置后使用手动恢复客户端重置模式手动恢复未同步的 Realm 数据。
手动恢复需要大量代码、模式让步和自定义冲突解决逻辑。 仅当您不能丢失未同步的数据并且其他自动客户端重置方法不符合您的使用案例时,才应手动恢复未同步的 Realm 数据。
有关其他可用客户端重置模式的更多信息,请参阅重置客户端 Realm。
手动恢复的细节在很大程度上取决于您的应用程序和模式。 但是,有一些技术可以帮助执行手动恢复。 按对象策略跟踪更改部分演示了在客户端重置期间恢复未同步更改的一种方法。
警告
避免在生产中进行破坏性模式更改
不要指望在中断性模式更改后恢复所有未同步的数据。 保留用户数据的最佳方法是永远不要进行破坏性的模式更改。
重要
中断性模式更改需要更新应用模式
发生重大模式更改后:
所有客户端都必须执行客户端重置。
您必须更新中断性模式更改影响的客户端模型。
按对象策略跟踪更改
通过按对象追踪更改手动客户端重置数据恢复策略,您可以恢复已写入客户端 Realm 文件但尚未同步到后端的数据。
在此策略中,您将为每个对象模型添加“上次更新时间”,以跟踪每个对象上次更改的时间。 我们将观察以确定 Realm 上次将其状态上传到后端的时间。
当后端调用客户端重置时,查找自上次与后端同步以来删除、创建或更新的对象。 然后将该数据从备份 Realm 复制到新 Realm。
以下步骤演示了在高级别上实施该流程:
客户端重置错误:您的应用程序从后端收到客户端重置错误代码。
策略实施:SDK 调用您的策略实施。
关闭该 Realm 的所有实例:关闭正在经历客户端重置的所有打开的 Realm 实例。 如果您的应用程序架构使此操作变得困难(例如,如果您的应用程序在整个应用程序的侦听器中同时使用许多域实例),则重新启动应用程序可能会更容易。您可以通过编程方式或通过在对话框中直接请求用户来执行此操作。
将 Realm 移动到备份文件:调用Realm.App.Sync.initiateClientReset() 静态方法。 此方法将客户端 Realm 文件的当前副本移动到备份文件中。
打开 Realm的新实例 :使用典型的同步配置打开 Realm 的新实例。 如果您的应用程序使用多个Realm,您可以从备份文件名称中识别正在经历客户端重置的域。
从后端下载所有 Realm 数据:在继续之前,请先下载 Realm 中的整个数据集。 这是SyncConfiguration对象的默认行为。
打开 域 备份:使用作为参数传递给
SyncConfiguration.error
回调函数的error.config
对象。迁移未同步的更改:查询备份 Realm 以获取要恢复的数据。 相应地在新 Realm 中插入、删除或更新数据。
例子
此示例演示如何实施追踪对象更改客户端重置数据恢复策略。
注意
此示例的限制
此示例仅适用于具有包含单个 Realm 对象类型的单个 Realm 的应用程序。 对于每个额外的对象类型,您都需要添加另一个同步侦听器,如“在单独 Realm 中跟踪同步”部分中所述。
此示例追踪每个对象的上次更新时间。因此,如果在上次成功同步备份 Realm 之后更新了任何字段,则恢复操作会覆盖新 Realm 中的整个对象。 这可能会使用该客户端的旧数据覆盖其他客户端更新的字段。如果您的 Realm 对象包含多个包含重要数据的字段,请考虑改为追踪每个字段的上次更新时间,并单独恢复每个字段。
有关通过数据恢复执行手动客户端重置的其他方法的更多信息,请参阅“替代策略”部分。
配置 Realm 以使用手动客户端重置
在 Realm 的SyncConfiguration中,设立clientReset
字段设置为手动模式,并包含error
回调函数。 您将在创建回调以处理客户端重置部分中定义错误回调函数。
const config = { schema: [DogSchema], sync: { user: app.currentUser, partitionValue: "MyPartitionValue", clientReset: { mode: "manual", }, error: handleSyncError, // callback function defined later }, };
在单独的 Realm 中跟踪同步
仅知道对象何时更改并不足以在客户端重置期间恢复数据。 您还需要知道 Realm 上次成功完成同步的时间。 此示例实现使用名为LastSynced
的单独 Realm 中的单例对象,并搭配变更侦听器来记录 Realm 何时成功完成同步。
定义你的 LastSynced Realm 以追踪你的域的最新同步时间。
const LastSyncedSchema = { name: "LastSynced", properties: { realmTracked: "string", timestamp: "int?", }, primaryKey: "realmTracked", }; const lastSyncedConfig = { schema: [LastSyncedSchema] }; const lastSyncedRealm = await Realm.open(lastSyncedConfig); lastSyncedRealm.write(() => { lastSyncedRealm.create("LastSynced", { realmTracked: "Dog", }); });
注册变更侦听器以订阅 Dog collection的变更。仅当同步会话已连接并且所有本地更改均已与服务器同步时,才更新 LastSynced 对象。
// Listens for changes to the Dogs collection realm.objects("Dog").addListener(async () => { // only update LastSynced if sync session is connected // and all local changes are synced if (realm.syncSession.isConnected()) { await realm.syncSession.uploadAllLocalChanges(); lastSyncedRealm.write(() => { lastSyncedRealm.create("LastSynced", { realmTracked: "Dog", timestamp: Date.now(), }); }); } });
创建回调以处理客户端重置
现在您已记录应用程序中所有对象的更新时间以及应用程序上次完成同步的时间,是时候实施手动恢复过程了。 此示例处理两个主要恢复操作:
从备份 Realm 恢复未同步的插入和更新
从新 Realm 中删除之前从备份 Realm 中删除的对象
您可以在下面的代码示例中了解这些操作的实现。
async function handleSyncError(_session, error) { if (error.name === "ClientReset") { const realmPath = realm.path; // realm.path will not be accessible after realm.close() realm.close(); // you must close all realms before proceeding // pass your realm app instance and realm path to initiateClientReset() Realm.App.Sync.initiateClientReset(app, realmPath); // Redownload the realm realm = await Realm.open(config); const oldRealm = await Realm.open(error.config); const lastSyncedTime = lastSyncedRealm.objectForPrimaryKey( "LastSynced", "Dog" ).timestamp; const unsyncedDogs = oldRealm .objects("Dog") .filtered(`lastUpdated > ${lastSyncedTime}`); // add unsynced dogs to synced realm realm.write(() => { unsyncedDogs.forEach((dog) => { realm.create("Dog", dog, "modified"); }); }); // delete dogs from synced realm that were deleted locally const syncedDogs = realm .objects("Dog") .filtered(`lastUpdated <= ${lastSyncedTime}`); realm.write(() => { syncedDogs.forEach((dog) => { if (!oldRealm.objectForPrimaryKey("Dog", dog._id)) { realm.delete(dog); } }); }); // make sure everything syncs and close old realm await realm.syncSession.uploadAllLocalChanges(); oldRealm.close(); } else { console.log(`Received error ${error.message}`); } }
替代策略
可能的替代实现包括:
用备份覆盖整个后端:在没有“上次更新时间”或“上次同步时间”的情况下,将备份域中的所有对象更新或插入到新域中。这种方法无法恢复未同步的删除。 这种方法会覆盖自上次同步以来其他客户端写入后端的所有数据。 建议用于只有一个用户写入每个 Realm 的应用程序。
按字段跟踪更改:跟踪每个字段的“上次更新时间”,而不是跟踪每个对象的“上次更新时间”。 使用此逻辑单独更新字段,以避免旧数据覆盖来自其他客户端的字段写入。 建议用于每个对象具有多个字段的应用程序,其中必须在字段级别解决冲突。
追踪与对象分开的更新:无需在每个对象的模式中追踪“上次更新时间”,而是在您的模式中创建另一个名为
Updates
的模型。每次任何对象中的任何字段(Updates
除外)更新时,记录主键、字段和更新时间。 在客户端重置期间,使用备份域中该字段的最新值“重写”在“上次同步时间”之后发生的所有Update
事件。这种方法应该会复制新域中所有未同步的本地更改,而不会用过时数据覆盖任何字段。但是,如果应用程序频繁写入,则存储collection的更新可能会很高。推荐用于不希望将“lastUpdated”字段添加到对象模型的应用程序。