Importante
La recuperación manual es manual
La recuperación manual requiere una cantidad considerable de código, concesiones de esquema y una lógica de resolución de conflictos personalizada. Si su aplicación puede permitir la pérdida de datos no sincronizados durante el restablecimiento del cliente, pruebe la estrategia de descartar cambios no sincronizados.
Advertencia
Evite realizar cambios disruptivos en el esquema en producción
No espere recuperar todos los datos no sincronizados tras un cambio de esquema disruptivo. La mejor manera de preservar los datos del usuario es no realizar nunca un cambio de esquema disruptivo (también llamado destructivo).
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:
Todos los clientes deben realizar un reinicio del cliente.
Debe actualizar los modelos de cliente afectados por el cambio de esquema importante.
La estrategia de restablecimiento del cliente para recuperar manualmente los cambios no sincronizados permite a los desarrolladores recuperar datos ya escritos en el archivo de dominio del cliente, pero aún no sincronizados con el backend. Los siguientes pasos ilustran el proceso a grandes rasgos:
Error de restablecimiento de cliente: su aplicación recibe un código de error de restablecimiento de cliente desde el backend.
Implementación de la estrategia: el SDK la denomina implementación de su estrategia.
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.
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.Abrir una nueva instancia del dominio: Abra una nueva instancia del dominio con su configuración de sincronización habitual. Si su aplicación utiliza varios dominios, puede identificar el dominio que experimenta un restablecimiento de cliente a partir del nombre del archivo de copia de seguridad.
Descargue todos los datos del dominio desde el backend: Descargue todos los datos del dominio antes de continuar. Si su configuración de sincronización no especifica la opción waitForInitialRemoteData(), puede llamar a SyncSession.downloadAllServerChanges() después de abrir el dominio.
Abrir la copia de seguridad del dominio: Use el método getBackupRealmConfiguration() del proporcionado
ClientResetRequiredErrorpara 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.Migrar cambios no sincronizados: Consultar el dominio de respaldo para obtener datos que se recuperarán. Insertar, eliminar o actualizar datos en el nuevo dominio según corresponda.
Para gestionar los restablecimientos de clientes con la estrategia de "recuperar manualmente los cambios no sincronizados", pase una instancia de ManuallyRecoverUnsyncedChangesStrategy al método de compilación defaultSyncClientResetStrategy() al crear una instancia App de. Su ManuallyRecoverUnsyncedChangesStrategy instancia de debe implementar los siguientes métodos:
onClientReset(): se llama cuando el SDK recibe un error de reinicio del cliente desde el 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() { 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
Implementación de handleManualReset()
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.
Los detalles de la recuperación manual dependen en gran medida de la aplicación y el esquema. Sin embargo, existen algunas técnicas que pueden ser útiles con la mayoría de las recuperaciones manuales. El siguiente ejemplo de implementación muestra un método para recuperar cambios no sincronizados desde un dominio de copia de seguridad.
Ejemplo
Este ejemplo agrega una "Hora de última actualización" a cada modelo de objeto para rastrear la última modificación de cada objeto. Observaremos el dominio para determinar la "Hora de última sincronización" y así determinar cuándo cargó su estado al backend por última vez. Luego, podemos encontrar objetos eliminados, creados o actualizados desde la última sincronización con el backend y copiar esos datos del dominio de respaldo al nuevo.
Seguimiento de actualizaciones de objetos
Normalmente, no es posible detectar cuándo se modificó por última vez un objeto Realm. Esto dificulta determinar qué cambios se sincronizaron con el backend. Al agregar una marca de tiempo a las clases de objetos Realm y actualizarla a la hora actual cada vez que se produce un cambio, puede realizar un seguimiento de cuándo se modificaron los objetos:
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") } }
Seguimiento de sincronizaciones exitosas
Saber cuándo se modificaron los objetos no es suficiente para recuperar datos durante el reinicio de un cliente. También es necesario saber cuándo el dominio completó una sincronización correctamente por última vez. Esta implementación de ejemplo utiliza un objeto singleton llamado LastSynced en el dominio, junto con un detector de progreso de carga, para registrar cuándo un dominio termina de sincronizarse correctamente.
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() {} }
Puedes usar SyncSession.addUploadProgressListener() para escuchar los eventos de progreso de carga en tu.App Implementa onChange() para gestionar estos eventos. Llama a Progress.isTransferComplete() para comprobar si la carga se ha completado. Cuando isTransferComplete() devuelve "true", todas las actualizaciones, inserciones y LastSynced LastSynced eliminaciones del lado del cliente en el dominio se han sincronizado correctamente con el backend, y puedes LastSynced actualizar la LastSynced hora de a la hora actual. Para evitar que se repita al10actualizar la hora de, no actualices la hora de si han pasado menos de, por ejemplo, ms desde la última vez que la actualizaste.
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(); });
// 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() }
Recuperación manual con hora de última actualización y hora de última sincronización
Ahora que ha registrado las horas de actualización de todos los objetos de su aplicación, así como la última vez que se realizó una sincronización, es hora de implementar el proceso de recuperación manual. Este ejemplo gestiona dos operaciones de recuperación principales:
Restaurar inserciones y actualizaciones no sincronizadas desde el ámbito de respaldo
eliminar objetos del nuevo reino que se eliminaron previamente del reino de respaldo
Puede seguir la implementación de estas operaciones en los ejemplos de código a continuación.
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
Este ejemplo está simplificado
Este ejemplo registra la última actualización de cada objeto. Como resultado, la operación de recuperación sobrescribe todo el objeto en el nuevo dominio si algún campo se actualizó después de la última sincronización correcta del dominio de respaldo. Esto podría sobrescribir los campos actualizados por otros clientes con datos antiguos de este cliente. Si los objetos de su dominio contienen varios campos con datos importantes, considere registrar la última actualización de cada campo y recuperar cada campo individualmente.
Implementaciones alternativas
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.
Rastrear las actualizaciones por separado de los objetos: En lugar de rastrear la "última hora de actualización" en el esquema de cada objeto, cree otro modelo en su esquema
Updatesllamado. Cada vez que se actualice un campo de cualquier objeto (excepto),Updatesregistre la clave principal, el campo y la hora de la actualización. Durante el reinicio de un cliente, reescriba todos losUpdateeventos ocurridos después de la "última hora de sincronización" utilizando el último valor de ese campo en el dominio de respaldo. Este enfoque debería replicar todos los cambios locales no sincronizados en el nuevo dominio sin sobrescribir ningún campo con datos obsoletos. Sin embargo, almacenar la colección de actualizaciones podría resultar costoso si su aplicación escribe con frecuencia. Se recomienda para aplicaciones donde no se desea agregar campos "lastUpdated" a los modelos de objetos.