Recuperação de dados de reinício do cliente - Java SDK
Nesta página
Importante
A recuperação manual é manual
A recuperação manual requer quantidades significativas de código, concessões de esquema e lógica personalizada de resolução de conflitos. Se a sua aplicação puder acomodar a perda de dados não sincronizados durante um reinício do cliente, tente a estratégia de reinício do cliente para descartar alterações não sincronizadas.
Aviso
Evite fazer alterações significativas no esquema na produção
Não espere recuperar todos os dados não sincronizados após uma alteração significativa no esquema. A melhor maneira de preservar os dados do usuário é nunca fazer uma alteração significativa - também chamada de destrutiva - no esquema.
Importante
As alterações de esquema de ruptura exigem uma atualização de esquema de aplicativo
Após uma alteração significativa no esquema:
Todos os clientes devem fazer um reinício do cliente.
Você deve atualizar os modelos de cliente afetados pela alteração do esquema de quebra.
A estratégia de reinício do cliente recuperar manualmente alterações não sincronizadas oferece aos desenvolvedores a oportunidade de recuperar dados já gravados no Arquivo de Realm do cliente, mas ainda não sincronizados com o backend. As seguintes etapas demonstram o processo em um alto nível:
Erro de reinício do cliente: sua aplicação recebe um código de erro de reinício do cliente do backend.
Implementação da estratégia: o SDK chama a implementação da estratégia.
Feche todas as instâncias do Realm: Feche todas as instâncias abertas do Realm que estão enfrentando o reinício do cliente. Se a arquitetura da sua aplicação dificultar isso (por exemplo, se a sua aplicação usar muitas Instância de Realm simultaneamente em ouvintes em toda a aplicação), pode ser mais fácil reiniciar a aplicação. Você pode fazer isso programaticamente ou por meio de uma solicitação direta ao usuário em uma caixa de diálogo.
Mover o Realm para um arquivo de backup: chame o método
executeClientReset()
do ClientResetRequiredError fornecido . Este método move a cópia atual do arquivo de Realm do cliente para um arquivo de backup.Abra uma nova instância do Realm: Abra uma nova instância do Realm usando sua configuração de sincronização típica. Se a sua aplicação usar vários Realms, você poderá identificar o Realm que está enfrentando um reinício do cliente a partir do nome do arquivo de backup.
Baixar todos os dados do Realm do backend: Baixe todo o conjunto de dados no Realm antes de prosseguir. Se sua configuração de sincronização não especificar a opção waitForInitialRemoteData() , você poderá chamar SyncSession.downloadAllServerChanges() depois de abrir o Realm.
Abrir o backup do domínio: Use o método getBackupRealmConfiguration() do
ClientResetRequiredError
fornecido para abrir uma instância do arquivo de domínio do cliente a partir do arquivo de backup. Você deve abrir essa instância como DynamicRealm, um tipo de realm que usa pesquisas de campo de texto para todo o acesso aos dados.Migrar alterações não sincronizadas: Query o Realm de backup para obter dados a serem recuperados. Insira, exclua ou atualize dados no novo Realm adequadamente.
Para lidar com as redefinições do cliente com a estratégia "recuperar manualmente alterações não sincronizadas", passe uma instância de ManuallyRecoverUnsyncedChangesStrategy para o método construtor defaultSyncClientResetStrategy() ao instanciar o App
. Sua instância ManuallyRecoverUnsyncedChangesStrategy
deve implementar os seguintes métodos:
onClientReset()
: chamado quando o SDK recebe um erro de reinício do cliente do backend.
O exemplo a seguir implementa essa estratégia:
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())
Observação
Implementação handleManualReset()
Este exemplo de reinício do cliente chama um método separado que lida com a lógica específica do reinício do cliente. Continue lendo as seções abaixo para ver um exemplo de implementação.
As especificidades da recuperação manual dependem muito do seu aplicativo e do seu esquema. No entanto, existem algumas técnicas que podem ajudar na maioria das recuperações manuais. O exemplo de implementação a seguir demonstra um método de recuperação de alterações não sincronizadas de um Realm de backup.
Exemplo
Este exemplo adiciona um "Horário da última atualização" a cada modelo de objeto para acompanhar quando cada objeto foi alterado pela última vez. Observaremos o domínio em busca do "horário da última sincronização" para determinar quando o domínio carregou pela última vez seu estado para o back-end. Em seguida, podemos encontrar objetos que foram excluídos, criados ou atualizados desde a última sincronização com o backend e copiar esses dados do domínio de backup para o novo domínio.
Rastrear atualizações de objetos
Normalmente, não há como detectar quando um Objeto de Realm foi modificado pela última vez. Isso torna difícil determinar quais alterações foram sincronizadas com o backend. Ao adicionar um registro de data/hora às suas classes de Objeto de Realm e atualizar esse registro de data/hora para a hora atual sempre que ocorre uma alteração, você pode acompanhar quando os objeto foram alterados:
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") } }
Rastrear sincronizações bem-sucedidas
Apenas saber quando o objeto foi alterado não é suficiente para recuperar dados durante um reinício do cliente. Você também precisa saber quando o Realm concluiu pela última vez uma sincronização com sucesso. Este exemplo de implementação usa um objeto singleton chamado LastSynced
no Realm, emparelhado com um ouvinte de progresso do upload, para registrar sempre que um Realm terminar de sincronizar com êxito.
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() {} }
Você pode usar SyncSession.addUploadProgressListener() para ouvir eventos de progresso de upload em seu App
. Implemente o onChange()
para lidar com esses eventos. Ligue para Progress.isTransferComplete() para verificar se o upload foi concluído. Quando isTransferComplete()
retorna true, todas as atualizações, inserções e exclusões do lado do cliente no Realm foram sincronizadas com sucesso com o backend e você pode atualizar o horário LastSynced
para o horário atual. Para evitar que LastSynced
faça um loop de atualizações para o tempo LastSynced
, não atualize o tempo LastSynced
se tiver passado menos de, digamos, 10ms desde a última vez que você atualizou o tempo.
Registre seu ouvinte de progresso em ProgressMode.INDEfiniTELY para inscrever seu ouvinte em todos os eventos de progresso de upload futuros, em vez de apenas nos eventos de progresso do upload atual.
// 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() }
Recuperação manual com a última hora atualizada e a última hora sincronizada
Agora que você registrou os tempos de atualização de todos os objetos do seu aplicativo, bem como a última vez que seu aplicativo concluiu uma sincronização, é hora de implementar o processo de recuperação manual. Este exemplo lida com duas operações de recuperação principais:
restaurando inserções e atualizações não sincronizadas do Realm de backup
excluindo objeto do novo Realm que foram excluídos anteriormente do Realm de backup
Você pode acompanhar a implementação dessas operações nas amostras de código abaixo.
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() } }
Observação
Este exemplo é simplificado
Este exemplo acompanha a última vez que cada objeto foi atualizado. Como resultado, a operação de recuperação substitui todo o objeto no novo Realm se algum campo foi atualizado após a última sincronização bem-sucedida do Realm de backup. Isso pode substituir campos atualizados por outros clientes por dados antigos desse cliente. Se o seu Objeto de Realm contiver vários campo contendo dados importantes, considere acompanhar a hora da última atualização de cada campo e recuperar cada campo individualmente.
Implementações alternativas
Outras implementações possíveis incluem:
Substitua todo o backend pelo estado do backup: sem "hora da última atualização" ou "hora da última sincronização",
insertOrUpdate()
todos os objeto do Realm de backup no novo Realm. Não há como recuperar exclusões não sincronizadas com essa abordagem. Essa abordagem substitui todos os dados gravados no backend por outros clientes desde a última sincronização. Recomendado para aplicação em que apenas um usuário grava em cada Realm.Rastrear alterações por campo: em vez de rastrear uma "hora da última atualização" para cada objeto, acompanhe a "hora da última atualização" de cada campo. Atualize os campos individualmente usando essa lógica para evitar a substituição de gravações de campos de outros clientes por dados antigos. Recomendado para aplicativos com muitos campos por objeto onde os conflitos devem ser resolvidos no nível do campo.
Acompanhe as atualizações separadamente dos objetos: em vez de acompanhar uma "hora da última atualização" no esquema de cada objeto, crie outro modelo no seu esquema chamado
Updates
. Toda vez que qualquer campo em qualquer objeto (alémUpdates
) for atualizado, registre a chave primária, o campo e a hora da atualização. Durante um reinício do cliente, "reescreva" todos os eventoUpdate
que ocorreram após o "último tempo sincronizado" usando o valor mais recente desse campo no Realm de backup. Essa abordagem deve replicar todas as alterações locais não sincronizadas no novo Realm sem substituir nenhum campo por dados obsoletos. No entanto, armazenar a collection de atualizações pode se tornar caro se sua aplicação for gravado com frequência. Recomendado para aplicativos em que adicionar campos "lastUpdated" a modelos de objetos não é desejável.