手動クライアントリセットデータリカバリ - Java SDK
重要
手動復元は手動
手動リカバリには、大量のコード、スキーマの認可、カスタム競合解決ロジックが必要です。 アプリケーションがクライアントリセット中に同期されていないデータの損失に対応できる場合は、代わりに 同期されていない変更のクライアントリセット戦略 を試してください。
警告
本番環境で重大なスキーマ変更を行わない
スキーマの重大な変更後に、同期されていないデータがすべて回復することは期待しないでください。 ユーザー データを保持する最良の方法は、スキーマの重大な変更とも呼ばれる、スキーマの変更をまったく行わないことです。
重要
スキーマの重大な変更にはアプリスキーマの更新が必要
スキーマの重大な変更後:
すべてのクライアントがクライアント リセットを実行する必要があります。
スキーマの重大な変更の影響を受けるクライアントモデルを更新する必要があります。
同期されていない変更のクライアント リセット戦略により、開発者はクライアント Realmファイルにすでに書き込まれたがバックエンドにまだ同期されていないデータを回復する機会が得られます。 次の手順は、 プロセスを高レベルで示します。
クライアント リセット エラー: アプリケーションはバックエンドからクライアント リセット エラー コードを受信します。
戦略実装: SDK は戦略実装を呼び出します。
Realm のすべてのインスタンスを閉じる : クライアントがリセットされている Realm の開いているインスタンスをすべて閉じます。 アプリケーション アーキテクチャによってこれが困難な場合(たとえば、アプリがアプリケーション全体でリスナーにおいて多くの Realm インスタンスを同時に使用する場合)は、アプリケーションを再起動した方が簡単な場合があります。 これはプログラムによって、またはダイアログでユーザーに直接リクエストすることで行うことができます。
レルムを バックアップ ファイル に移動します。提供された ClientResetRequiredErrorの
executeClientReset()
メソッドを呼び出します。 このメソッドは、クライアント Realm ファイルの現在のコピーをバックアップ ファイルに移動します。Realm の新しいインスタンスを開きます: 一般的な同期構成を使用して、Realm の新しいインスタンスを開きます。 アプリケーションで複数の Realm が使用されている場合は、バックアップ ファイル名からクライアントがリセットされる Realm を識別できます。
バックエンドからすべての Realm データをダウンロード : 続行する前に、Realm 内のデータ セット全体をダウンロードします。 同期構成でwaitForInitial remoteData()オプションが指定されていない場合は、 SyncSession. DownloadAllServerChecks( )を呼び出すことができます Realm を開きます。
Realmバックアップを開きます : 提供された
ClientResetRequiredError
のgetBackupRealmConfiguration()メソッドを使用して、バックアップ ファイルからクライアント Realm ファイルのインスタンスを開きます。 このインスタンスは、すべてのデータ アクセスにテキスト フィールド検索を使用するRealmのタイプである Synapse Realm として開く必要があります。同期されていない変更を移行 : バックアップ邦土をクエリして、復元するデータを確認します。 必要に応じて新しい Realm でデータを挿入、削除、更新します。
「同期されていない変更を手動で回復する」戦略でクライアントのリセットを処理するには、 App
をインスタンス化するときにManuallyRecoverUnsyncedchangesStrateyのインスタンスをdefaultSyncClientResetStratey()ビルダ メソッドに渡します。 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() { 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())
注意
candidateReset() の実装
このクライアント リセットの例では、クライアント リセットの特定のロジックを処理する別のメソッドを呼び出します。 の実装例については、以下のセクションを読み続けます。
手動リカバリの詳細は、アプリケーションとスキーマによって大きく異なります。 ただし、ほとんどの手動リカバリに役立つ手法がいくつかあります。 次の実装例は、バックアップ Realm から同期されていない変更を回復する方法の 1 つを示しています。
例
この例では、各オブジェクト モデルに「最終アップデート時間」を追加して、各オブジェクトが最後に変更されたタイミングを追跡します。 Realm で「Last Synced Time」を監視して、Realm がその状態をバックエンドに最後にアップロードしたかを判断します。 次に、バックエンドとの前回の同期以降に削除、作成、または更新されたオブジェクトを見つけ、そのデータをバックアップ Realm から新しい Realm にコピーします。
オブジェクトへの更新の追跡
通常、Realm オブジェクトが最後に変更されたタイミングを検出する方法はありません。 そのため、どの変更がバックエンドに同期されたかを判断するのが困難になります。 Realm オブジェクト クラスにタイムスタンプを追加し、変更が発生するたびにそのタイムスタンプを現在の時刻に更新することで、オブジェクトが変更されたタイミングを追跡できます。
import org.bson.types.ObjectId; import io.realm.DynamicRealmObject; import io.realm.RealmObject; import io.realm.annotations.PrimaryKey; public class Potato extends RealmObject { 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; } }
import io.realm.DynamicRealmObject import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.bson.types.ObjectId open class Potato : RealmObject { 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") } }
import org.bson.types.ObjectId; import io.realm.DynamicRealmObject; import io.realm.RealmObject; import io.realm.annotations.PrimaryKey; public class Onion extends RealmObject { 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; } }
import io.realm.DynamicRealmObject import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.bson.types.ObjectId open class Onion : RealmObject { 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") } }
import org.bson.types.ObjectId; import io.realm.DynamicRealmObject; import io.realm.RealmObject; import io.realm.annotations.PrimaryKey; public class Rice extends RealmObject { 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; } }
import io.realm.DynamicRealmObject import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.bson.types.ObjectId open class Rice : RealmObject { 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 の同期が正常に完了するたびに記録します。
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; 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; } }
import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.bson.types.ObjectId open class LastSynced : RealmObject { var timestamp: Long? = null 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.addUploadProgressLister()を使用できます は、 App
でアップロード進行状況イベントをリッスンします。 これらのイベントを処理するにはonChange()
を実装します。 Processing.isTransferComplete ()の呼び出し アップロードが完了したかどうかを確認します。 isTransferComplete()
が true を返すと、Realm 内のすべてのクライアント側の更新、挿入、削除がバックエンドに正常に同期され、 LastSynced
時間を現在の時刻に更新できます。 LastSynced
がLastSynced
時間への更新でループ処理を行わないようにするには、時間を最後に更新してから10ミリ秒未満である場合は、 LastSynced
時間を更新 しない でください。
プログレス リスナーをProgressMode.INDFI Atlasで登録し、現在のアップロードの進行状況イベントだけでなく、将来のすべてのアップロード進行状況イベントにリスナーをサブスクライブします。
// 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() }
最終更新時間と最後同期時間による手動リカバリ
アプリケーション内のすべてのオブジェクトの更新時間と、アプリケーションが最後に同期を完了した時刻を記録したので、手動回復プロセスを実装します。 この例では、2 つの主要なリカバリ操作を処理しています。
バックアップ邦土から同期されていない挿入と更新を復元する
以前にバックアップ 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 オブジェクトに重要なデータを含む複数のフィールドが含まれている場合は、代わりに各フィールドの最終更新時間を追跡し、各フィールドを個別に回復することを検討してください。
代替の実装
その他の実装には、次のようなものがあります。
バックエンド全体をバックアップ状態 で上書きします。「最終アップデート時間」または「最後に同期された時間」がない場合、
insertOrUpdate()
すべてのオブジェクトはバックアップRealmから新しいRealmに移行します。 このアプローチでは、同期されていない削除を復元する方法はありません。 このアプローチでは、最後の同期以降に他のクライアントによってバックエンドに書き込まれたすべてのデータが上書きされます。 1 人のユーザーのみが各 Realm に書込むアプリケーションに推奨されます。フィールド で変更を追跡: すべてのオブジェクトで「最終アップデート時間」を追跡する代わりに、すべてのフィールドの「最終アップデート時間」を追跡します。 このロジックを使用してフィールドを個別に更新し、古いデータを持つ他のクライアントからのフィールド書込みを上書きしないようにします。 フィールド レベルで競合を解決する必要がある、オブジェクトごとに多数のフィールドを持つアプリケーションに推奨されます。
オブジェクト とは別にアップデートを追跡します。各オブジェクトのスキーマで「最終更新時間」を追跡する代わりに、スキーマに
Updates
という別のモデルを作成します。 オブジェクトの任意のフィールド(Updates
以外)が更新されるたびに、プライマリキー、フィールド、更新時間を記録します。 クライアント リセット中に、バックアップ Realm 内のそのフィールドの最新値を使用して、「最終同期時間」後に発生したすべてのUpdate
イベントを「再書き込み」します。 このアプローチでは、古いデータを含むフィールドを上書きすることなく、新しい Realm 内の同期されていないすべてのローカル変更が複製されます。 ただし、アプリケーションが頻繁に書込みを行う場合は、更新のコレクションを保存するとコストが高くなる可能性があります。 オブジェクトモデルへの "lastUpdated" フィールドの追加が望ましくないアプリケーションに推奨されます。