Docs 菜单
Docs 主页
/ /
Atlas Device SDKs
/ /

手动客户端重置数据恢复 — Java SDK

在此页面上

  • 例子
  • 追踪对象的更新
  • 追踪成功的同步
  • 使用上次更新时间和上次同步时间进行手动恢复
  • 替代实施

重要

手动恢复为 Manual

手动恢复需要大量代码、模式让步和自定义冲突解决逻辑。 如果您的应用程序可以容纳在客户端重置期间丢失未同步的数据,请尝试改用丢弃未同步的更改客户端重置策略。

警告

避免在生产中进行破坏性模式更改

不要指望在中断性模式更改后恢复所有未同步的数据。 保留用户数据的最佳方法是永远不要进行中断性(也称为破坏性)模式更改。

重要

中断性模式更改需要更新应用模式

发生重大模式更改后:

  • 所有客户端都必须执行客户端重置。

  • 您必须更新中断性模式更改影响的客户端模型。

手动恢复未同步更改客户端重置策略使开发者有机会恢复已写入客户端 Realm 文件但尚未同步到后端的数据。以下步骤概括地演示了该过程:

  1. 客户端重置错误:您的应用程序从后端收到客户端重置错误代码。

  2. 策略实施:SDK 调用您的策略实施。

  3. 关闭该 Realm 的所有实例:关闭正在经历客户端重置的所有打开的 Realm 实例。 如果您的应用程序架构使此操作变得困难(例如,如果您的应用程序在整个应用程序的侦听器中同时使用许多域实例),则重新启动应用程序可能会更容易。您可以通过编程方式或通过在对话框中直接请求用户来执行此操作。

  4. 将域移动到备份文件:调用所提供的 ClientResetRequiredError executeClientReset()方法。 此方法将客户端域文件的当前副本移动到备份文件中。

  5. 打开 Realm的新实例 :使用典型的同步配置打开 Realm 的新实例。 如果您的应用程序使用多个Realm,您可以从备份文件名称中识别正在经历客户端重置的域。

  6. 从后端下载所有域数据:在继续之前,请先下载域中的整个数据设立。 如果您的同步配置未指定waitForInitialRemoteData()选项,您可以调用SyncSession.downloadAllServerChanges() 打开域后。

  7. 打开 Realm 备份:使用提供的ClientResetRequiredErrorgetBackupRealmConfiguration()方法从备份文件中打开客户端 Realm 文件的实例。 您必须将此实例作为DynamicRealm打开,这是一种使用文本字段查找进行所有数据访问的 Realm。

  8. 迁移未同步的更改:查询备份 Realm 以获取要恢复的数据。 相应地在新 Realm 中插入、删除或更新数据。

要使用“手动恢复未同步更改”策略处理客户端重置,请在实例化App时将ManuallyRecoverUnsyncedChangesStrategy的实例传递给defaultSyncClientResetStrategy()构建器方法。 您的ManuallyRecoverUnsyncedChangesStrategy实例必须实现以下方法:

  • onClientReset():当 SDK 从后端收到客户端重置错误时调用。

以下示例实施了该策略:

String appID = YOUR_APP_ID; // replace this with your App ID
final App app = new App(new AppConfiguration.Builder(appID)
.defaultSyncClientResetStrategy(new ManuallyRecoverUnsyncedChangesStrategy() {
@Override
public void onClientReset(SyncSession session, ClientResetRequiredError error) {
Log.v("EXAMPLE", "Executing manual client reset handler");
handleManualReset(session.getUser().getApp(), session, error);
}
})
.build());
val appID: String = YOUR_APP_ID // replace this with your App ID
val app = App(AppConfiguration.Builder(appID)
.defaultSyncClientResetStrategy { session, error ->
Log.v("EXAMPLE", "Executing manual client reset handler")
handleManualReset(session.user.app, session, error)
}
.build())

注意

handleManualReset() 实施

此客户端重置示例调用一个单独的方法来处理客户端重置的特定逻辑。 继续阅读以下部分,了解实施示例。

手动恢复的具体细节在很大程度上取决于您的应用程序和模式。 但是,有一些技术可以帮助完成大多数手动恢复。 以下示例实现演示了一种从备份域恢复未同步更改的方法。

此示例向每个对象模型添加“上次更新时间”,以追踪每个对象上次更改的时间。我们将观察 Realm 的“上次同步时间”,以确定 Realm 上次将其状态上传到后端的时间。 然后,我们可以找到自上次与后端同步以来删除、创建或更新的对象,并将该数据从备份 Realm 复制到新 Realm。

通常,无法检测 Realm 对象的最后一次修改时间。 因此很难确定哪些更改已同步到后端。 通过向 Realm 对象类添加时间戳,并在发生更改时将该时间戳更新为当前时间,您可以跟踪对象的更改时间:

Potato.java
import org.bson.types.ObjectId;
import io.realm.DynamicRealmObject;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Potato extends RealmObject {
@PrimaryKey
private ObjectId _id;
private Long lastUpdated;
private String species;
public Potato(ObjectId id, String species) {
this._id = id;
this.lastUpdated = System.currentTimeMillis();
this.species = species;
}
public Potato() { this.lastUpdated = System.currentTimeMillis(); }
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
public Potato(DynamicRealmObject obj) {
this._id = obj.getObjectId("_id");
this.species = obj.getString("species");
this.lastUpdated = obj.getLong("lastUpdated");
}
public ObjectId getId() { return _id; }
public String getSpecies() { return species; }
public void setSpecies(String species) {
this.species = species;
this.lastUpdated = System.currentTimeMillis();
}
public Long getLastUpdated() { return lastUpdated; }
public void setLastUpdated(Long lastUpdated) {
this.lastUpdated = lastUpdated;
}
}
Potato.kt
import io.realm.DynamicRealmObject
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.bson.types.ObjectId
open class Potato : RealmObject {
@PrimaryKey
var _id: ObjectId? = null
var lastUpdated: Long
var species: String? = null
set(species: String?) {
field = species
lastUpdated = System.currentTimeMillis()
}
constructor(id: ObjectId?, species: String?) {
this._id = id
lastUpdated = System.currentTimeMillis()
this.species = species
}
constructor() {
lastUpdated = System.currentTimeMillis()
}
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
constructor(obj: DynamicRealmObject) {
_id = obj.getObjectId("_id")
species = obj.getString("species")
lastUpdated = obj.getLong("lastUpdated")
}
}
Onion.java
import org.bson.types.ObjectId;
import io.realm.DynamicRealmObject;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Onion extends RealmObject {
@PrimaryKey
public ObjectId _id;
public Long lastUpdated;
public String varietal;
public Onion(ObjectId id, String varietal) {
this._id = id;
this.lastUpdated = System.currentTimeMillis();
this.varietal = varietal;
}
public Onion() { this.lastUpdated = System.currentTimeMillis(); }
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
public Onion(DynamicRealmObject obj) {
this._id = obj.getObjectId("_id");
this.varietal = obj.getString("varietal");
this.lastUpdated = obj.getLong("lastUpdated");
}
public ObjectId getId() { return _id; }
public String getVarietal() { return varietal; }
public void setVarietal(String varietal) {
this.varietal = varietal;
this.lastUpdated = System.currentTimeMillis();
}
public Long getLastUpdated() { return lastUpdated; }
public void setLastUpdated(Long lastUpdated) {
this.lastUpdated = lastUpdated;
}
}
Onion.kt
import io.realm.DynamicRealmObject
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.bson.types.ObjectId
open class Onion : RealmObject {
@PrimaryKey
var _id: ObjectId? = null
var lastUpdated: Long
var varietal: String? = null
set(varietal: String?) {
lastUpdated = System.currentTimeMillis()
field = varietal
}
constructor(id: ObjectId?, varietal: String?) {
this._id = id
lastUpdated = System.currentTimeMillis()
this.varietal = varietal
}
constructor() {
lastUpdated = System.currentTimeMillis()
}
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
constructor(obj: DynamicRealmObject) {
_id = obj.getObjectId("_id")
varietal = obj.getString("varietal")
lastUpdated = obj.getLong("lastUpdated")
}
}
ice.java
import org.bson.types.ObjectId;
import io.realm.DynamicRealmObject;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Rice extends RealmObject {
@PrimaryKey
protected ObjectId _id;
protected Long lastUpdated;
protected String style;
public Rice(ObjectId id, String style) {
this._id = id;
this.lastUpdated = System.currentTimeMillis();
this.style = style;
}
public Rice() { this.lastUpdated = System.currentTimeMillis(); }
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
public Rice(DynamicRealmObject obj) {
this._id = obj.getObjectId("_id");
this.style = obj.getString("style");
this.lastUpdated = obj.getLong("lastUpdated");
}
public ObjectId getId() { return _id; }
public String getStyle() { return style; }
public void setStyle(String style) {
this.style = style;
this.lastUpdated = System.currentTimeMillis();
}
public Long getLastUpdated() { return lastUpdated; }
public void setLastUpdated(Long lastUpdated) {
this.lastUpdated = lastUpdated;
}
}
ice.kt
import io.realm.DynamicRealmObject
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.bson.types.ObjectId
open class Rice : RealmObject {
@PrimaryKey
var _id: ObjectId? = null
var lastUpdated: Long
var style: String? = null
set(style: String?) {
field = style
lastUpdated = System.currentTimeMillis()
}
constructor(id: ObjectId?, style: String?) {
this._id = id
lastUpdated = System.currentTimeMillis()
this.style = style
}
constructor() {
lastUpdated = System.currentTimeMillis()
}
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
constructor(obj: DynamicRealmObject) {
_id = obj.getObjectId("_id")
style = obj.getString("style")
lastUpdated = obj.getLong("lastUpdated")
}
}

仅知道对象何时更改并不足以在客户端重置期间恢复数据。 您还需要知道 Realm 上次成功完成同步的时间。 此示例实现在 Realm 中使用名为LastSynced的单例对象,并搭配上传进度侦听器,以便在 Realm 成功完成同步时进行记录。

LastSynced.java
import org.bson.types.ObjectId;
import java.util.Date;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class LastSynced extends RealmObject {
protected Long timestamp;
@PrimaryKey
protected ObjectId _id = null;
// only one instance per realm -- enforce by forcing a single objectid value on all instances
public LastSynced(Long timestamp) {
this.timestamp = timestamp;
}
public LastSynced() {}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
public ObjectId get_id() {
return _id;
}
}
LastSynced.kt
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.bson.types.ObjectId
open class LastSynced : RealmObject {
var timestamp: Long? = null
@PrimaryKey
var _id: ObjectId? = null
protected set(id: ObjectId?) {}
// only one instance per realm -- enforce by forcing a single objectid value on all instances
constructor(timestamp: Long?) {
this.timestamp = timestamp
}
constructor() {}
}

您可以使用SyncSession.addUploadProgressListener() 监听App中的上传进度事件。 实施onChange()来处理这些事件。 调用Progress.isTransferComplete() 以检查上传是否已完成。 当isTransferComplete()返回 true 时, 域中的所有客户端更新、插入和删除均已成功同步到后端,您可以将LastSynced时间更新为当前时间。 为防止LastSyncedLastSynced时间更新时发生循环,如果自上次更新时间以来的时间小于10毫秒,请勿更新LastSynced时间。

使用ProgressMode.INDEFINITELY注册进度侦听器,使侦听器订阅所有未来的上传进度事件,而不仅仅是当前上传的进度事件。

// use a "last synced" singleton in the realm to keep track of when the
// realm last successfully completed a sync
app.getSync().getSession(config)
.addUploadProgressListener(ProgressMode.INDEFINITELY, progress -> {
// get the last synced time. Create an instance if it does not already exist.
Realm notificationRealm = Realm.getInstance(config);
LastSynced lastSynced =
notificationRealm.where(LastSynced.class).findFirst();
if (lastSynced == null) {
notificationRealm.executeTransaction(transactionRealm ->
transactionRealm.createObject(LastSynced.class,
new ObjectId()).setTimestamp(System.currentTimeMillis()));
}
// only update the "last synced" time when ALL client data has uploaded
// avoid repeatedly setting "last synced" every time we update "last synced"
// by checking if the current "last synced" time was within the last 10ms
if(progress.isTransferComplete() &&
System.currentTimeMillis() > lastSynced.getTimestamp() + 10) {
notificationRealm.executeTransaction(transactionRealm -> {
transactionRealm.where(LastSynced.class)
.findFirst()
.setTimestamp(System.currentTimeMillis());
Log.v("EXAMPLE", "Updating last synced time to: "
+ System.currentTimeMillis());
});
Log.v("EXAMPLE", "Updated last synced time to: " +
lastSynced.getTimestamp());
}
notificationRealm.close();
});
上传进度监听器
// use a "last synced" singleton in the realm to keep track of when the
// realm last successfully completed a sync
app.sync.getSession(config)
.addUploadProgressListener(ProgressMode.INDEFINITELY) { progress: Progress ->
// get the last synced time. Create an instance if it does not already exist.
val notificationRealm = Realm.getInstance(config)
val lastSynced =
notificationRealm.where(LastSynced::class.java).findFirst()
if (lastSynced == null) {
notificationRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.createObject(
LastSynced::class.java,
ObjectId()
).timestamp = System.currentTimeMillis()
}
}
// only update the "last synced" time when ALL client data has uploaded
// avoid repeatedly setting "last synced" every time we update "last synced"
// by checking if the current "last synced" time was within the last 10ms
if (progress.isTransferComplete &&
System.currentTimeMillis() > lastSynced?.timestamp?.plus(10) ?: 0
) {
notificationRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.where(LastSynced::class.java)
.findFirst()
?.timestamp = System.currentTimeMillis()
Log.v(
"EXAMPLE", "Updating last synced time to: "
+ System.currentTimeMillis()
)
}
Log.v(
"EXAMPLE", "Updated last synced time to: " +
lastSynced!!.timestamp
)
}
notificationRealm.close()
}

现在您已记录应用程序中所有对象的更新时间以及应用程序上次完成同步的时间,是时候实施手动恢复过程了。 此示例处理两个主要恢复操作:

  • 从备份 Realm 恢复未同步的插入和更新

  • 从新 Realm 中删除之前从备份 Realm 中删除的对象

您可以在下面的代码示例中了解这些操作的实现。

public void handleManualReset(App app, SyncSession session, ClientResetRequiredError error) {
Log.w("EXAMPLE", "Beginning manual reset recovery.");
// Close all instances of the realm -- this application only uses one
globalRealm.close();
try {
Log.w("EXAMPLE", "About to execute the client reset.");
// Move the realm to a backup file -- execute the client reset
error.executeClientReset();
Log.w("EXAMPLE", "Executed the client reset.");
} catch (IllegalStateException e) {
Log.e("EXAMPLE", "Failed to execute the client reset: " + e.getMessage());
// The client reset can only proceed if there are no open realms.
// if execution failed, ask the user to restart the app, and we'll client reset
// when we first open the app connection.
AlertDialog restartDialog = new AlertDialog.Builder(activity)
.setMessage("Sync error. Restart the application to resume sync.")
.setTitle("Restart to Continue")
.create();
restartDialog.show();
}
// Open new instance of the realm. This initializes a new file for the new realm
// and downloads the backend state. Do this in a background thread so we can wait
// for server changes to fully download.
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
Realm newRealm = Realm.getInstance(globalConfig);
// Download all realm data from the backend -- ensure that the backend state is
// fully downloaded before proceeding
try {
app.getSync().getSession(globalConfig).downloadAllServerChanges(10000,
TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.w("EXAMPLE", "Opened a fresh instance of the realm.");
// Open the the realm backup -- as a dynamic realm
// (no formal schema; access all data through field lookups)
DynamicRealm backupRealm = DynamicRealm.getInstance(error.getBackupRealmConfiguration());
Log.w("EXAMPLE", "Opened the backup realm.");
// To only migrate unsynced data,
// you'll need to know the last time the realm synced.
// you can keep track of successful sync connections
// locally in an object in the realm
DynamicRealmObject lastSuccessfulSynced =
backupRealm.where("LastSynced").findFirst();
Long lastSuccessfulSyncTime =
lastSuccessfulSynced.getLong("timestamp");
// Migrate unsynced changes: move data from the backup
// instance of the realm to the new "fresh" instance fetched from the backend.
// This includes:
// - copying any objects that updated, but didn't sync from the
// backup realm to the new realm.
// - re-deleting any objects that were deleted locally while we were offline
// Insert any unsynced updated objects to the new realm
// NOTE: this will overwrite any changes made by other clients
// to those objects since the last sync.
// Applications that require finer-grained conflict resolution
// should use custom logic instead.
// This example keeps track of when the object last updated by also writing
// to a "lastUpdated" field on write operations.
RealmQuery<DynamicRealmObject> potatoQuery =
backupRealm.where("Potato")
.greaterThan("lastUpdated", lastSuccessfulSyncTime);
RealmQuery<DynamicRealmObject> onionQuery =
backupRealm.where("Onion")
.greaterThan("lastUpdated", lastSuccessfulSyncTime);
RealmQuery<DynamicRealmObject> riceQuery =
backupRealm.where("Rice")
.greaterThan("lastUpdated", lastSuccessfulSyncTime);
// insert the backup version of all unsynced object updates + creates into the new realm
// NOTE: this process will overwrite writes from other clients, potentially overwriting
// data in fields not modified in the backup realm. Use with caution. If this does not
// meet your application's needs, consider keeping track of the last write for each
// field individually (and recovering them individually, per-field).
for(DynamicRealmObject potato : potatoQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + potato.getString("species"));
newRealm.executeTransaction(transactionRealm ->
transactionRealm.insertOrUpdate(new Potato(potato)));
}
for(DynamicRealmObject onion : onionQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + onion.getString("varietal"));
newRealm.executeTransaction(transactionRealm ->
transactionRealm.insertOrUpdate(new Onion(onion)));
}
for(DynamicRealmObject rice : riceQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + rice.getString("style"));
newRealm.executeTransaction(transactionRealm ->
transactionRealm.insertOrUpdate(new Rice(rice)));
}
// re-delete unsynced deletions from the new realm
// caveat: if an object has been updated SINCE the last update from this client,
// (from another client) this does not delete that object. This doesn't match
// realm's usual "deletes always win" behavior but it isn't possible to
// distinguish between:
// - objects that were deleted from this client after the last sync
// - objects that were created by another client after the last sync
// So instead of deleting innocent objects created by other clients, we let
// other client updates "win" in this case.
// This means that previously deleted (but unsynced) objects could reappear on this
// client after the client reset event.
// get all the ids of objects that haven't been updated since the last client sync
// (anything that's been updated since the last sync should not be deleted)
// -- could be new object, or an object this client deleted but another client modified
Set<ObjectId> allNewPotatoIds = newRealm.where(Potato.class)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream().map(Potato::getId).collect(Collectors.toSet());
Set<ObjectId> allNewOnionIds = newRealm.where(Onion.class)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream().map(Onion::getId).collect(Collectors.toSet());
Set<ObjectId> allNewRiceIds = newRealm.where(Rice.class)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream().map(Rice::getId).collect(Collectors.toSet());
Log.v("EXAMPLE", "number of potatoes in fresh realm" +
"that have not been updated since last sync: " + allNewPotatoIds.size());
Log.v("EXAMPLE", "number of onions in fresh realm" +
"that have not been updated since last sync: " + allNewOnionIds.size());
Log.v("EXAMPLE", "number of rices in fresh realm" +
"that have not been updated since last sync: " + allNewRiceIds.size());
// get all the ids of objects in the backup realm
Set<ObjectId> allOldPotatoIds = backupRealm.where("Potato")
.findAll().stream().map(obj -> obj.getObjectId("_id"))
.collect(Collectors.toSet());
Set<ObjectId> allOldOnionIds = backupRealm.where("Onion")
.findAll().stream().map(obj -> obj.getObjectId("_id"))
.collect(Collectors.toSet());
Set<ObjectId> allOldRiceIds = backupRealm.where("Rice")
.findAll().stream().map(obj -> obj.getObjectId("_id"))
.collect(Collectors.toSet());
Log.v("EXAMPLE", "number of potatoes in the old realm: " +
allOldPotatoIds.size());
Log.v("EXAMPLE", "number of onions in the old realm: " +
allOldOnionIds.size());
Log.v("EXAMPLE", "number of rices in the old realm: " +
allOldRiceIds.size());
// Get the set of:
// all objects in the new realm
// - that have not been updated since last sync
// - that are not in the backup realm
// Those objects were deleted from the backup realm sometime after the last sync.
Set<ObjectId> unsyncedPotatoDeletions = allNewPotatoIds.stream()
.filter(((Predicate<ObjectId>)(allOldPotatoIds::contains)).negate())
.collect(Collectors.toSet());
Set<ObjectId> unsyncedOnionDeletions = allNewOnionIds.stream()
.filter(((Predicate<ObjectId>)(allOldOnionIds::contains)).negate())
.collect(Collectors.toSet());
Set<ObjectId> unsyncedRiceDeletions = allNewRiceIds.stream()
.filter(((Predicate<ObjectId>)(allOldRiceIds::contains)).negate())
.collect(Collectors.toSet());
Log.v("EXAMPLE", "Number of potatos to re-delete: "
+ unsyncedPotatoDeletions.size());
Log.v("EXAMPLE", "Number of onions to re-delete: "
+ unsyncedOnionDeletions.size());
Log.v("EXAMPLE", "Number of rices to re-delete: "
+ unsyncedRiceDeletions.size());
// perform "re-deletions"
for(ObjectId id: unsyncedPotatoDeletions) {
Log.w("EXAMPLE", "Deleting " + unsyncedPotatoDeletions.size()
+ " potato objects.");
newRealm.executeTransaction(transactionRealm -> {
transactionRealm.where(Potato.class).equalTo("_id", id)
.findAll().deleteAllFromRealm();
});
}
for(ObjectId id: unsyncedOnionDeletions) {
Log.w("EXAMPLE", "Deleting " + unsyncedOnionDeletions.size()
+ " onion objects.");
newRealm.executeTransaction(transactionRealm -> {
transactionRealm.where(Onion.class).equalTo("_id", id)
.findAll().deleteAllFromRealm();
});
}
for(ObjectId id: unsyncedRiceDeletions) {
Log.w("EXAMPLE", "Deleting " + unsyncedRiceDeletions.size()
+ " rice objects.");
newRealm.executeTransaction(transactionRealm -> {
transactionRealm.where(Rice.class).equalTo("_id", id)
.findAll().deleteAllFromRealm();
});
}
// Output the state of the freshly downloaded realm, after recovering local data.
Log.v("EXAMPLE", "Number of potato objects in the new realm: "
+ newRealm.where(Potato.class).findAll().size());
Log.v("EXAMPLE", "Number of onion objects in the new realm: "
+ newRealm.where(Onion.class).findAll().size());
Log.v("EXAMPLE", "Number of rice objects in the new realm: "
+ newRealm.where(Rice.class).findAll().size());
// close the realms
backupRealm.close();
newRealm.close();
});
// execute the recovery logic on a background thread
try {
executor.awaitTermination(20000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
fun handleManualReset(app: App, session: SyncSession?, error: ClientResetRequiredError) {
Log.w("EXAMPLE", "Beginning manual reset recovery.")
// Close all instances of the realm -- this application only uses one
globalRealm!!.close()
try {
Log.w("EXAMPLE", "About to execute the client reset.")
// Move the realm to a backup file: execute the client reset
error.executeClientReset()
Log.w("EXAMPLE", "Executed the client reset.")
} catch (e: IllegalStateException) {
Log.e("EXAMPLE", "Failed to execute the client reset: " + e.message)
// The client reset can only proceed if there are no open realms.
// if execution failed, ask the user to restart the app, and we'll client reset
// when we first open the app connection.
val restartDialog = AlertDialog.Builder(activity)
.setMessage("Sync error. Restart the application to resume sync.")
.setTitle("Restart to Continue")
.create()
restartDialog.show()
}
// Open new instance of the realm. This initializes a new file for the new realm
// and downloads the backend state. Do this in a background thread so we can wait
// for server changes to fully download.
val executor = Executors.newSingleThreadExecutor()
executor.execute {
val newRealm = Realm.getInstance(globalConfig)
// Download all realm data from the backend -- ensure that the backend state is
// fully downloaded before proceeding
try {
app.sync.getSession(globalConfig)
.downloadAllServerChanges(10000, TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) {
e.printStackTrace()
}
Log.w("EXAMPLE", "Opened a fresh instance of the realm.")
// Open the the realm backup -- as a dynamic realm
// (no formal schema; access all data through field lookups)
val backupRealm =
DynamicRealm.getInstance(error.backupRealmConfiguration)
Log.w("EXAMPLE", "Opened the backup realm.")
// To only migrate unsynced data,
// you'll need to know the last time the realm synced.
// you can keep track of successful sync connections
// locally in an object in the realm
val lastSuccessfulSynced =
backupRealm.where("LastSynced").findFirst()
val lastSuccessfulSyncTime = lastSuccessfulSynced!!.getLong("timestamp")
// Migrate unsynced changes: move data from the backup
// instance of the realm to the new "fresh" instance fetched from the backend.
// This includes:
// - copying any objects that updated, but didn't sync from the
// backup realm to the new realm.
// - re-deleting any objects that were deleted locally while we were offline
// Insert any unsynced updated objects to the new realm
// NOTE: this will overwrite any changes made by other clients
// to those objects since the last sync.
// Applications that require finer-grained conflict resolution
// should use custom logic instead.
// This example keeps track of when the object last updated by also writing
// to a "lastUpdated" field on write operations.
val potatoQuery = backupRealm.where("Potato")
.greaterThan("lastUpdated", lastSuccessfulSyncTime)
val onionQuery = backupRealm.where("Onion")
.greaterThan("lastUpdated", lastSuccessfulSyncTime)
val riceQuery = backupRealm.where("Rice")
.greaterThan("lastUpdated", lastSuccessfulSyncTime)
// insert the backup version of all unsynced object updates + creates into the new realm
// NOTE: this process will overwrite writes from other clients, potentially overwriting
// data in fields not modified in the backup realm. Use with caution. If this does not
// meet your application's needs, consider keeping track of the last write for each
// field individually (and recovering them individually, per-field).
for (potato in potatoQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + potato.getString("species"))
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.insertOrUpdate(
Potato(potato)
)
}
}
for (onion in onionQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + onion.getString("varietal"))
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.insertOrUpdate(
Onion(onion)
)
}
}
for (rice in riceQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + rice.getString("style"))
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.insertOrUpdate(
Rice(rice)
)
}
}
// re-delete unsynced deletions from the new realm
// caveat: if an object has been updated SINCE the last update from this client,
// (from another client) this does not delete that object. This doesn't match
// realm's usual "deletes always win" behavior but it isn't possible to
// distinguish between:
// - objects that were deleted from this client after the last sync
// - objects that were created by another client after the last sync
// So instead of deleting innocent objects created by other clients, we let
// other client updates "win" in this case.
// This means that previously deleted (but unsynced) objects could reappear on this
// client after the client reset event.
// get all the ids of objects that haven't been updated since the last client sync
// (anything that's been updated since the last sync should not be deleted)
// -- could be new object, or an object this client deleted but another client modified
val allNewPotatoIds =
newRealm.where(
Potato::class.java
)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream()
.map { obj: Potato -> obj._id }
.collect(Collectors.toSet())
val allNewOnionIds =
newRealm.where(
Onion::class.java
)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream()
.map { obj: Onion -> obj._id }
.collect(Collectors.toSet())
val allNewRiceIds =
newRealm.where(
Rice::class.java
)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream()
.map { obj: Rice -> obj._id }
.collect(Collectors.toSet())
Log.v(
"EXAMPLE", "number of potatoes in fresh realm" +
"that have not been updated since last sync: " + allNewPotatoIds.size
)
Log.v(
"EXAMPLE", "number of onions in fresh realm" +
"that have not been updated since last sync: " + allNewOnionIds.size
)
Log.v(
"EXAMPLE", "number of rices in fresh realm" +
"that have not been updated since last sync: " + allNewRiceIds.size
)
// get all the ids of objects in the backup realm
val allOldPotatoIds =
backupRealm.where("Potato")
.findAll().stream()
.map { obj: DynamicRealmObject ->
obj.getObjectId(
"_id"
)
}
.collect(Collectors.toSet())
val allOldOnionIds =
backupRealm.where("Onion")
.findAll().stream()
.map { obj: DynamicRealmObject ->
obj.getObjectId(
"_id"
)
}
.collect(Collectors.toSet())
val allOldRiceIds =
backupRealm.where("Rice")
.findAll().stream()
.map { obj: DynamicRealmObject ->
obj.getObjectId(
"_id"
)
}
.collect(Collectors.toSet())
Log.v("EXAMPLE", "number of potatoes in the backup realm: " +
allOldPotatoIds.size)
Log.v("EXAMPLE", "number of onions in the backup realm: " +
allOldOnionIds.size)
Log.v("EXAMPLE", "number of rices in the backup realm: " +
allOldRiceIds.size)
// Get the set of:
// all objects in the new realm
// - that have not been updated since last sync
// - that are not in the backup realm
// Those objects were deleted from the backup realm sometime after the last sync.
val unsyncedPotatoDeletions =
allNewPotatoIds.stream()
.filter(Predicate { o: ObjectId ->
allOldPotatoIds.contains(o)
}.negate())
.collect(Collectors.toSet())
val unsyncedOnionDeletions =
allNewOnionIds.stream()
.filter(Predicate { o: ObjectId ->
allOldOnionIds.contains(o)
}.negate())
.collect(Collectors.toSet())
val unsyncedRiceDeletions =
allNewRiceIds.stream()
.filter(Predicate { o: ObjectId ->
allOldRiceIds.contains(o)
}.negate())
.collect(Collectors.toSet())
Log.v("EXAMPLE", "Number of potatos to re-delete: "
+ unsyncedPotatoDeletions.size)
Log.v("EXAMPLE", "Number of onions to re-delete: "
+ unsyncedOnionDeletions.size)
Log.v("EXAMPLE", "Number of rices to re-delete: "
+ unsyncedRiceDeletions.size)
// perform "re-deletions"
for (id in unsyncedPotatoDeletions) {
Log.w(
"EXAMPLE",
"Deleting " + unsyncedPotatoDeletions.size + " potato objects."
)
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.where(
Potato::class.java
).equalTo("_id", id).findAll().deleteAllFromRealm()
}
}
for (id in unsyncedOnionDeletions) {
Log.w(
"EXAMPLE",
"Deleting " + unsyncedOnionDeletions.size + " onion objects."
)
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.where(
Onion::class.java
).equalTo("_id", id).findAll().deleteAllFromRealm()
}
}
for (id in unsyncedRiceDeletions) {
Log.w(
"EXAMPLE",
"Deleting " + unsyncedRiceDeletions.size + " rice objects."
)
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.where(
Rice::class.java
).equalTo("_id", id).findAll().deleteAllFromRealm()
}
}
// Output the state of the freshly downloaded realm, after recovering local data.
Log.v(
"EXAMPLE", "Number of potato objects in the new realm: "
+ newRealm.where(
Potato::class.java
).findAll().size
)
Log.v(
"EXAMPLE", "Number of onion objects in the new realm: "
+ newRealm.where(
Onion::class.java
).findAll().size
)
Log.v(
"EXAMPLE", "Number of rice objects in the new realm: "
+ newRealm.where(
Rice::class.java
).findAll().size
)
// close the realms
backupRealm.close()
newRealm.close()
}
// execute the recovery logic on a background thread
try {
executor.awaitTermination(20000, TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}

注意

此示例已简化

此示例追踪每个对象的上次更新时间。因此,如果在上次成功同步备份 Realm 之后更新了任何字段,则恢复操作会覆盖新 Realm 中的整个对象。 这可能会使用该客户端的旧数据覆盖其他客户端更新的字段。如果您的 Realm 对象包含多个包含重要数据的字段,请考虑改为追踪每个字段的上次更新时间,并单独恢复每个字段。

其他可能的实施包括:

  • 用备份状态覆盖整个后端:没有“上次更新时间”或“上次同步时间”, insertOrUpdate()所有对象从备份 Realm 到新 Realm。 这种方法无法恢复未同步的删除。 这种方法会覆盖自上次同步以来其他客户端写入后端的所有数据。 建议用于只有一个用户写入每个 Realm 的应用程序。

  • 按字段跟踪更改:跟踪每个字段的“上次更新时间”,而不是跟踪每个对象的“上次更新时间”。 使用此逻辑单独更新字段,以避免旧数据覆盖来自其他客户端的字段写入。 建议用于每个对象具有多个字段的应用程序,其中必须在字段级别解决冲突。

  • 追踪与对象分开的更新:无需在每个对象的模式中追踪“上次更新时间”,而是在您的模式中创建另一个名为Updates的模型。每次任何对象中的任何字段( Updates除外)更新时,记录主键、字段和更新时间。 在客户端重置期间,使用备份域中该字段的最新值“重写”在“上次同步时间”之后发生的所有Update事件。这种方法应该会复制新域中所有未同步的本地更改,而不会用过时数据覆盖任何字段。但是,如果应用程序频繁写入,则存储collection的更新可能会很高。推荐用于不希望将“lastUpdated”字段添加到对象模型的应用程序。

后退

客户端重置