Join us at MongoDB.local London on 7 May to unlock new possibilities for your data. Use WEB50 to save 50%.
Register now >
Docs Menu
Docs Home
/ /
Sync Data

Manual Client Reset Data Recovery - Java SDK

Importante

Manual Recovery is Manual

La recuperación manual requiere grandes cantidades de código, concesiones de esquema y lógica personalizada de resolución de conflictos. Si tu aplicación puede tolerar la pérdida de datos no sincronizados durante un restablecimiento del cliente, prueba la estrategia de restablecimiento del cliente de descartar cambios no sincronizados en su lugar.

Advertencia

Avoid Making Breaking Schema Changes in Production

Do not expect to recover all unsynced data after a breaking schema change. The best way to preserve user data is to never make a breaking - also called destructive - schema change at all.

Importante

Los cambios importantes en el esquema requieren una actualización del esquema de la aplicación

Después de un cambio de esquema importante:

  • All clients must perform a client reset.

  • Debe actualizar los modelos de cliente afectados por el cambio de esquema importante.

The manually recover unsynced changes client reset strategy gives developers the opportunity to recover data already written to the client realm file but not yet synced to the backend. The following steps demonstrate the process at a high level:

  1. Client reset error: Your application receives a client reset error code from the backend.

  2. Strategy implementation: The SDK calls your strategy implementation.

  3. Cerrar todas las instancias del dominio: Cierre todas las instancias abiertas del dominio que experimenta el reinicio del cliente. Si la arquitectura de su aplicación lo dificulta (por ejemplo, si utiliza varias instancias del dominio simultáneamente en escuchas de toda la aplicación), puede ser más sencillo reiniciar la aplicación. Puede hacerlo mediante programación o mediante una solicitud directa al usuario en un diálogo.

  4. Mueva el reino a un archivo de respaldo: llame al executeClientReset() método del proporcionado ClientResetRequiredError. Este método mueve la copia actual del archivo de dominio del cliente a un archivo de respaldo.

  5. Open new instance of the realm: Open a new instance of the realm using your typical sync configuration. If your application uses multiple realms, you can identify the realm experiencing a client reset from the backup file name.

  6. Descargar todos los datos del realm del backend: Descarga todo el conjunto de datos del realm antes de proceder. Si tu configuración de sincronización no especifica la opción waitForInitialRemoteData(), puedes llamar a SyncSession.downloadAllServerChanges() después de abrir el reino.

  7. Abrir la copia de seguridad del dominio: Use el método getBackupRealmConfiguration() del proporcionado ClientResetRequiredError para abrir una instancia del archivo de dominio del cliente desde el archivo de copia de seguridad. Debe abrir esta instancia como un DynamicRealm, un tipo de dominio que utiliza búsquedas en campos de texto para todo el acceso a los datos.

  8. Migrar cambios no sincronizados: Consulta el realm de copia de seguridad para recuperar datos. Inserta, borra o actualiza datos en el nuevo realm según sea necesario.

To handle client resets with the "manually recover unsynced changes" strategy, pass an instance of ManuallyRecoverUnsyncedChangesStrategy to the defaultSyncClientResetStrategy() builder method when you instantiate your App. Your ManuallyRecoverUnsyncedChangesStrategy instance must implement the following methods:

  • onClientReset(): called when the SDK receives a client reset error from the backend.

El siguiente ejemplo implementa esta estrategia:

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())

Nota

handleManualReset() Implementation

Este ejemplo de restablecimiento de cliente llama a un método independiente que gestiona la lógica específica del restablecimiento. Continúe leyendo las secciones a continuación para ver un ejemplo de implementación.

The specifics of manual recovery depend heavily upon your application and your schema. However, there are a few techniques that can help with most manual recoveries. The following example implementation demonstrates one method of recovering unsynced changes from a backup realm.

This example adds a "Last Updated Time" to each object model to track when each object last changed. We'll watch the realm for the "Last Synced Time" to determine when the realm last uploaded its state to the backend. Then, we can find objects that were deleted, created, or updated since the last sync with the backend, and copy that data from the backup realm to the new realm.

Por lo general, no hay forma de detectar cuándo se modificó por última vez un objeto de Realm. Esto dificulta determinar qué cambios se sincronizaron con el backend. Agregando una marca de tiempo a tus clases de objetos Realm y actualizando esa marca de tiempo a la actual cada vez que se produce un cambio, puedes rastrear cuándo se modificaron los objetos:

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")
}
}
Arroz.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;
}
}
Rice.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")
}
}

Just knowing when objects were changed isn't enough to recover data during a client reset. You also need to know when the realm last completed a sync successfully. This example implementation uses a singleton object called LastSynced in the realm, paired with an upload progress listener, to record whenever a realm finishes syncing successfully.

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() {}
}

You can use SyncSession.addUploadProgressListener() to listen for upload progress events in your App. Implement onChange() to handle these events. Call Progress.isTransferComplete() to check if the upload has completed. When isTransferComplete() returns true, all clientside updates, inserts, and deletes in the realm have successfully synced to the backend, and you can update the LastSynced time to the current time. To prevent LastSynced from looping on updates to the LastSynced time, don't update the LastSynced time if it's been less than, say, 10ms since you last updated the time.

Registre su oyente de progreso con ProgressMode.INDEFINITELY para suscribir a su oyente a todos los eventos de progreso de carga futuros, en lugar de solo a los eventos de progreso de carga actual.

// 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();
});
Upload Progress Listener
// 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()
}

Ahora que ya has registrado los tiempos de actualización de todos los objetos de la aplicación, así como la última vez que la aplicación completó una sincronización, es hora de implementar el proceso de recuperación manual. Este ejemplo gestiona dos operaciones principales de recuperación:

  • restaurando inserciones y actualizaciones no sincronizadas desde el realm de copias de seguridad

  • deleting objects from the new realm that were previously deleted from the backup realm

You can follow along with the implementation of these operations in the code samples below.

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()
}
}

Nota

This Example is Simplified

This example keeps track of the last time each object was updated. As a result, the recovery operation overwrites the entire object in the new realm if any field was updated after the last successful sync of the backup realm. This could overwrite fields updated by other clients with old data from this client. If your realm objects contain multiple fields containing important data, consider keeping track of the last updated time of each field instead, and recovering each field individually.

Otras posibles implementaciones incluyen:

  • Sobrescribir todo el backend con el estado de la copia de seguridad: sin "última actualización" ni "última sincronización", insertOrUpdate() todos los objetos del dominio de copia de seguridad en el nuevo dominio. Con este enfoque, no es posible recuperar las eliminaciones no sincronizadas. Este enfoque sobrescribe todos los datos escritos en el backend por otros clientes desde la última sincronización. Recomendado para aplicaciones donde solo un usuario escribe en cada dominio.

  • Rastrear cambios por campo: en lugar de rastrear la 'última vez de actualización' para cada objeto, rastrear la 'última vez de actualización' para cada campo. Actualizar los campos individualmente utilizando esta lógica para evitar sobrescribir los guardados de campos de otros clientes con datos antiguos. Recomendado para aplicaciones con muchos campos por objeto donde los conflictos deben resolverse a nivel de campo.

  • Track updates separately from objects: Instead of tracking a "last updated time" in the schema of each object, create another model in your schema called Updates. Every time any field in any object (besides Updates) updates, record the primary key, field, and time of the update. During a client reset, "re-write" all of the Update events that occurred after the "last synced time" using the latest value of that field in the backup realm. This approach should replicate all unsynced local changes in the new realm without overwriting any fields with stale data. However, storing the collection of updates could become expensive if your application writes frequently. Recommended for applications where adding "lastUpdated" fields to object models is undesirable.

Volver

Restablecimiento del cliente

En esta página