수동 클라이언트 재설정 데이터 복구 - Java SDK
중요
수동 복구는 수동입니다.
수동 복구에는 상당한 양의 코드, 스키마 양보 및 사용자 지정 충돌 해결 로직이 필요합니다. 애플리케이션에서 클라이언트 재설정 중 동기화되지 않은 데이터 손실을 수용할 수 있는 경우, 대신 동기화되지 않은 변경 사항 삭제 클라이언트 재설정 전략을 시도해 보세요.
경고
프로덕션 환경에서 손상적인 스키마 변경 방지
손상적인 스키마 변경 후 동기화되지 않은 모든 데이터를 복구할 것으로 기대하지 마세요. 사용자 데이터를 보존하는 가장 좋은 방법은 손상적인 스키마 변경(또는 파괴적인 스키마 변경)을 전혀 하지 않는 것입니다.
중요
손상적인 스키마 변경에는 앱 스키마 업데이트가 필요합니다.
손상적인 스키마 변경 후 다음을 수행합니다:
모든 클라이언트는 클라이언트 재설정을 수행해야 합니다.
손상적인 스키마 변경의 영향을 받는 클라이언트 모델을 업데이트해야 합니다.
동기화되지 않은 변경 사항 수동 복구 클라이언트 재설정 전략을 사용하면 개발자가 Realm 파일에 이미 기록되었지만 아직 백엔드에 동기화되지 않은 데이터를 복구할 수 있습니다. 다음 단계에서는 이 프로세스를 높은 수준으로 보여줍니다.
클라이언트 재설정 오류: 애플리케이션이 백엔드에서 클라이언트 재설정 오류 코드를 수신합니다.
전략 구현: SDK가 전략 구현을 호출합니다.
영역의 모든 인스턴스 닫기 : 클라이언트 재설정이 발생하는 영역의 열려 있는 모든 인스턴스를 닫습니다. 애플리케이션 아키텍처로 인해 이를 달성하기 어려운 경우(예: 앱이 애플리케이션 전체의 리스너에서 동시에 많은 Realm 인스턴스를 사용하는 경우) 애플리케이션을 다시 시작하는 것이 더 쉬울 수 있습니다. 프로그래밍 방식으로 또는 대화 상자에서 사용자에 대한 직접 요청을 통해 이 작업을 수행할 수 있습니다.
영역 을 백업 파일 로 이동: 제공된 ClientResetRequiredError 의
executeClientReset()
메서드를 호출합니다. 이 메서드는 클라이언트 영역 파일 의 현재 복사본을 백업 파일 로 이동합니다.영역 의 새 인스턴스 열기 : 일반적인 동기화 구성을 사용하여 영역의 새 인스턴스를 엽니다. 애플리케이션에서 여러 영역을 사용하는 경우 백업 파일 이름에서 클라이언트 재설정이 발생한 영역을 식별할 수 있습니다.
백엔드 에서 모든 영역 데이터 다운로드 : 계속하기 전에 영역 의 전체 데이터 설정하다 를 다운로드합니다. 동기화 구성에 waitForInitialRemoteData() 옵션이 지정되어 있지 않은 경우 SyncSession.downloadAllServerChanges() 를 호출할 수 있습니다. 영역 을 연 후 .
영역 백업 열기: 제공된
ClientResetRequiredError
의 getBackupRealmConfiguration() 메서드를 사용하여 백업 파일에서 클라이언트 영역 파일의 인스턴스를 엽니다. 이 인스턴스를 모든 데이터 액세스에 텍스트 필드 조회를 사용하는 영역 유형 DynamicRealm 으로 열어야 합니다.동기화되지 않은 변경 사항 마이그레이션: 복구할 데이터에 대한 백업 영역을 쿼리합니다. 그에 따라 새 영역의 데이터를 삽입, 삭제 또는 업데이트합니다.
'동기화되지 않은 변경 사항 수동 복구' 전략으로 클라이언트 재설정을 처리하려면 App
인스턴스화할 때 수동 으로 RecoverUnsyncedChangesStrategy 인스턴스를 defaultSyncClientResetStrategy() 빌더 메서드에 전달합니다. ManuallyRecoverUnsyncedChangesStrategy
인스턴스는 다음 메서드를 구현해야 합니다.
onClientReset()
: SDK가 백엔드에서 클라이언트 재설정 오류를 수신할 때 호출됩니다.
다음 예제에서는 이 전략을 구현합니다:
String appID = YOUR_APP_ID; // replace this with your App ID final App app = new App(new AppConfiguration.Builder(appID) .defaultSyncClientResetStrategy(new ManuallyRecoverUnsyncedChangesStrategy() { public void onClientReset(SyncSession session, ClientResetRequiredError error) { Log.v("EXAMPLE", "Executing manual client reset handler"); handleManualReset(session.getUser().getApp(), session, error); } }) .build());
val appID: String = YOUR_APP_ID // replace this with your App ID val app = App(AppConfiguration.Builder(appID) .defaultSyncClientResetStrategy { session, error -> Log.v("EXAMPLE", "Executing manual client reset handler") handleManualReset(session.user.app, session, error) } .build())
참고
handleManualReset() 구현
이 클라이언트 재설정 예시 에서는 클라이언트 재설정 의 특정 로직을 처리하는 별도의 메서드를 호출합니다. 구현 예시 를 보려면 아래 섹션을 계속 읽어보세요.
수동 복구의 세부 사항은 애플리케이션과 스키마에 따라 크게 달라집니다. 그러나 대부분의 수동 복구에 도움이 될 수 있는 몇 가지 기술이 있습니다. 다음 구현 예시는 백업 Realm에서 동기화되지 않은 변경 사항을 복구하는 한 가지 방법을 보여줍니다.
예시
이 예제에서는 각 Realm 객체 모델에 '마지막 업데이트 시간'을 추가하여 각 객체가 마지막으로 변경된 시간을 추적합니다. 우리는 "마지막 동기화 시간" 동안 영역을 관찰하여 영역이 백엔드에 상태를 마지막으로 업로드한 시점을 확인합니다. 그런 다음 백엔드와의 마지막 동기화 이후 삭제, 생성 또는 업데이트된 객체를 찾아 해당 데이터를 백업 영역에서 새 영역으로 복사할 수 있습니다.
객체 업데이트 추적
일반적으로 Realm 객체가 마지막으로 수정된 시점을 감지할 수 있는 방법은 없습니다. 이로 인해 백엔드에 동기화된 변경 사항을 확인하기가 어렵습니다. Realm 객체 클래스에 타임스탬프를 추가하고 변경이 발생할 때마다 해당 타임스탬프를 현재 시간으로 업데이트하면 객체가 변경된 시점을 추적할 수 있습니다.
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") } }
성공한 동기화 추적
객체가 변경된 시점을 알고 있다는 것만으로는 클라이언트 재설정 중에 데이터를 복구하는 데 충분하지 않습니다. 또한 영역이 마지막으로 동기화를 성공적으로 완료한 시점을 알아야 합니다. 이 구현 예시에서는 업로드 진행 리스너와 쌍을 이루는 영역의 LastSynced
싱글톤 객체를 사용하여 영역의 동기화가 성공적으로 완료될 때마다 기록합니다.
import org.bson.types.ObjectId; import java.util.Date; import io.realm.RealmObject; import io.realm.annotations.PrimaryKey; public class LastSynced extends RealmObject { protected Long timestamp; protected ObjectId _id = null; // only one instance per realm -- enforce by forcing a single objectid value on all instances public LastSynced(Long timestamp) { this.timestamp = timestamp; } public LastSynced() {} public Long getTimestamp() { return timestamp; } public void setTimestamp(Long timestamp) { this.timestamp = timestamp; } public ObjectId get_id() { return _id; } }
import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.bson.types.ObjectId open class LastSynced : RealmObject { var timestamp: Long? = null var _id: ObjectId? = null protected set(id: ObjectId?) {} // only one instance per realm -- enforce by forcing a single objectid value on all instances constructor(timestamp: Long?) { this.timestamp = timestamp } constructor() {} }
SyncSession.addUploadProgressListener() App
에서 업로드 진행 이벤트를 수신 대기합니다. 이러한 이벤트를 처리하다 하려면 onChange()
를 구현하세요. Progress.isTransferComplete() 호출 을(를) 클릭하여 업로드가 완료되었는지 확인합니다. isTransferComplete()
가 true를 반환하면 영역 의 모든 클라이언트 사이드 업데이트, 삽입 및 삭제가 백엔드 에 성공적으로 동기화된 것이므로 LastSynced
시간을 현재 시간으로 업데이트 할 수 있습니다. LastSynced
가 LastSynced
시간에 대한 업데이트를 반복하지 않도록 하려면 마지막으로 시간을 업데이트한 이후로 예를 들어 10ms 미만인 경우 LastSynced
시간을 업데이트 하지 마세요.
진행률 리스너를 ProgressMode.INDEfiniteLY 에 구독 합니다.
// use a "last synced" singleton in the realm to keep track of when the // realm last successfully completed a sync app.getSync().getSession(config) .addUploadProgressListener(ProgressMode.INDEFINITELY, progress -> { // get the last synced time. Create an instance if it does not already exist. Realm notificationRealm = Realm.getInstance(config); LastSynced lastSynced = notificationRealm.where(LastSynced.class).findFirst(); if (lastSynced == null) { notificationRealm.executeTransaction(transactionRealm -> transactionRealm.createObject(LastSynced.class, new ObjectId()).setTimestamp(System.currentTimeMillis())); } // only update the "last synced" time when ALL client data has uploaded // avoid repeatedly setting "last synced" every time we update "last synced" // by checking if the current "last synced" time was within the last 10ms if(progress.isTransferComplete() && System.currentTimeMillis() > lastSynced.getTimestamp() + 10) { notificationRealm.executeTransaction(transactionRealm -> { transactionRealm.where(LastSynced.class) .findFirst() .setTimestamp(System.currentTimeMillis()); Log.v("EXAMPLE", "Updating last synced time to: " + System.currentTimeMillis()); }); Log.v("EXAMPLE", "Updated last synced time to: " + lastSynced.getTimestamp()); } notificationRealm.close(); });
// use a "last synced" singleton in the realm to keep track of when the // realm last successfully completed a sync app.sync.getSession(config) .addUploadProgressListener(ProgressMode.INDEFINITELY) { progress: Progress -> // get the last synced time. Create an instance if it does not already exist. val notificationRealm = Realm.getInstance(config) val lastSynced = notificationRealm.where(LastSynced::class.java).findFirst() if (lastSynced == null) { notificationRealm.executeTransaction { transactionRealm: Realm -> transactionRealm.createObject( LastSynced::class.java, ObjectId() ).timestamp = System.currentTimeMillis() } } // only update the "last synced" time when ALL client data has uploaded // avoid repeatedly setting "last synced" every time we update "last synced" // by checking if the current "last synced" time was within the last 10ms if (progress.isTransferComplete && System.currentTimeMillis() > lastSynced?.timestamp?.plus(10) ?: 0 ) { notificationRealm.executeTransaction { transactionRealm: Realm -> transactionRealm.where(LastSynced::class.java) .findFirst() ?.timestamp = System.currentTimeMillis() Log.v( "EXAMPLE", "Updating last synced time to: " + System.currentTimeMillis() ) } Log.v( "EXAMPLE", "Updated last synced time to: " + lastSynced!!.timestamp ) } notificationRealm.close() }
마지막 업데이트 시간 및 마지막 동기화 시간을 사용한 수동 복구
이제 애플리케이션의 모든 객체에 대한 업데이트 시간과 애플리케이션이 마지막으로 동기화를 완료한 시간을 기록했으므로 수동 복구 프로세스를 구현할 차례입니다. 이 예제에서는 두 가지 주요 복구 작업을 처리합니다.
백업 영역에서 동기화되지 않은 삽입 및 업데이트 복원
백업 영역에서 이전에 삭제된 객체를 새 영역에서 삭제
아래 코드 샘플에서 이러한 작업의 구현을 따라갈 수 있습니다.
public void handleManualReset(App app, SyncSession session, ClientResetRequiredError error) { Log.w("EXAMPLE", "Beginning manual reset recovery."); // Close all instances of the realm -- this application only uses one globalRealm.close(); try { Log.w("EXAMPLE", "About to execute the client reset."); // Move the realm to a backup file -- execute the client reset error.executeClientReset(); Log.w("EXAMPLE", "Executed the client reset."); } catch (IllegalStateException e) { Log.e("EXAMPLE", "Failed to execute the client reset: " + e.getMessage()); // The client reset can only proceed if there are no open realms. // if execution failed, ask the user to restart the app, and we'll client reset // when we first open the app connection. AlertDialog restartDialog = new AlertDialog.Builder(activity) .setMessage("Sync error. Restart the application to resume sync.") .setTitle("Restart to Continue") .create(); restartDialog.show(); } // Open new instance of the realm. This initializes a new file for the new realm // and downloads the backend state. Do this in a background thread so we can wait // for server changes to fully download. ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> { Realm newRealm = Realm.getInstance(globalConfig); // Download all realm data from the backend -- ensure that the backend state is // fully downloaded before proceeding try { app.getSync().getSession(globalConfig).downloadAllServerChanges(10000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } Log.w("EXAMPLE", "Opened a fresh instance of the realm."); // Open the the realm backup -- as a dynamic realm // (no formal schema; access all data through field lookups) DynamicRealm backupRealm = DynamicRealm.getInstance(error.getBackupRealmConfiguration()); Log.w("EXAMPLE", "Opened the backup realm."); // To only migrate unsynced data, // you'll need to know the last time the realm synced. // you can keep track of successful sync connections // locally in an object in the realm DynamicRealmObject lastSuccessfulSynced = backupRealm.where("LastSynced").findFirst(); Long lastSuccessfulSyncTime = lastSuccessfulSynced.getLong("timestamp"); // Migrate unsynced changes: move data from the backup // instance of the realm to the new "fresh" instance fetched from the backend. // This includes: // - copying any objects that updated, but didn't sync from the // backup realm to the new realm. // - re-deleting any objects that were deleted locally while we were offline // Insert any unsynced updated objects to the new realm // NOTE: this will overwrite any changes made by other clients // to those objects since the last sync. // Applications that require finer-grained conflict resolution // should use custom logic instead. // This example keeps track of when the object last updated by also writing // to a "lastUpdated" field on write operations. RealmQuery<DynamicRealmObject> potatoQuery = backupRealm.where("Potato") .greaterThan("lastUpdated", lastSuccessfulSyncTime); RealmQuery<DynamicRealmObject> onionQuery = backupRealm.where("Onion") .greaterThan("lastUpdated", lastSuccessfulSyncTime); RealmQuery<DynamicRealmObject> riceQuery = backupRealm.where("Rice") .greaterThan("lastUpdated", lastSuccessfulSyncTime); // insert the backup version of all unsynced object updates + creates into the new realm // NOTE: this process will overwrite writes from other clients, potentially overwriting // data in fields not modified in the backup realm. Use with caution. If this does not // meet your application's needs, consider keeping track of the last write for each // field individually (and recovering them individually, per-field). for(DynamicRealmObject potato : potatoQuery.findAll()) { Log.w("EXAMPLE", "Inserting: " + potato.getString("species")); newRealm.executeTransaction(transactionRealm -> transactionRealm.insertOrUpdate(new Potato(potato))); } for(DynamicRealmObject onion : onionQuery.findAll()) { Log.w("EXAMPLE", "Inserting: " + onion.getString("varietal")); newRealm.executeTransaction(transactionRealm -> transactionRealm.insertOrUpdate(new Onion(onion))); } for(DynamicRealmObject rice : riceQuery.findAll()) { Log.w("EXAMPLE", "Inserting: " + rice.getString("style")); newRealm.executeTransaction(transactionRealm -> transactionRealm.insertOrUpdate(new Rice(rice))); } // re-delete unsynced deletions from the new realm // caveat: if an object has been updated SINCE the last update from this client, // (from another client) this does not delete that object. This doesn't match // realm's usual "deletes always win" behavior but it isn't possible to // distinguish between: // - objects that were deleted from this client after the last sync // - objects that were created by another client after the last sync // So instead of deleting innocent objects created by other clients, we let // other client updates "win" in this case. // This means that previously deleted (but unsynced) objects could reappear on this // client after the client reset event. // get all the ids of objects that haven't been updated since the last client sync // (anything that's been updated since the last sync should not be deleted) // -- could be new object, or an object this client deleted but another client modified Set<ObjectId> allNewPotatoIds = newRealm.where(Potato.class) .lessThan("lastUpdated", lastSuccessfulSyncTime) .findAll().stream().map(Potato::getId).collect(Collectors.toSet()); Set<ObjectId> allNewOnionIds = newRealm.where(Onion.class) .lessThan("lastUpdated", lastSuccessfulSyncTime) .findAll().stream().map(Onion::getId).collect(Collectors.toSet()); Set<ObjectId> allNewRiceIds = newRealm.where(Rice.class) .lessThan("lastUpdated", lastSuccessfulSyncTime) .findAll().stream().map(Rice::getId).collect(Collectors.toSet()); Log.v("EXAMPLE", "number of potatoes in fresh realm" + "that have not been updated since last sync: " + allNewPotatoIds.size()); Log.v("EXAMPLE", "number of onions in fresh realm" + "that have not been updated since last sync: " + allNewOnionIds.size()); Log.v("EXAMPLE", "number of rices in fresh realm" + "that have not been updated since last sync: " + allNewRiceIds.size()); // get all the ids of objects in the backup realm Set<ObjectId> allOldPotatoIds = backupRealm.where("Potato") .findAll().stream().map(obj -> obj.getObjectId("_id")) .collect(Collectors.toSet()); Set<ObjectId> allOldOnionIds = backupRealm.where("Onion") .findAll().stream().map(obj -> obj.getObjectId("_id")) .collect(Collectors.toSet()); Set<ObjectId> allOldRiceIds = backupRealm.where("Rice") .findAll().stream().map(obj -> obj.getObjectId("_id")) .collect(Collectors.toSet()); Log.v("EXAMPLE", "number of potatoes in the old realm: " + allOldPotatoIds.size()); Log.v("EXAMPLE", "number of onions in the old realm: " + allOldOnionIds.size()); Log.v("EXAMPLE", "number of rices in the old realm: " + allOldRiceIds.size()); // Get the set of: // all objects in the new realm // - that have not been updated since last sync // - that are not in the backup realm // Those objects were deleted from the backup realm sometime after the last sync. Set<ObjectId> unsyncedPotatoDeletions = allNewPotatoIds.stream() .filter(((Predicate<ObjectId>)(allOldPotatoIds::contains)).negate()) .collect(Collectors.toSet()); Set<ObjectId> unsyncedOnionDeletions = allNewOnionIds.stream() .filter(((Predicate<ObjectId>)(allOldOnionIds::contains)).negate()) .collect(Collectors.toSet()); Set<ObjectId> unsyncedRiceDeletions = allNewRiceIds.stream() .filter(((Predicate<ObjectId>)(allOldRiceIds::contains)).negate()) .collect(Collectors.toSet()); Log.v("EXAMPLE", "Number of potatos to re-delete: " + unsyncedPotatoDeletions.size()); Log.v("EXAMPLE", "Number of onions to re-delete: " + unsyncedOnionDeletions.size()); Log.v("EXAMPLE", "Number of rices to re-delete: " + unsyncedRiceDeletions.size()); // perform "re-deletions" for(ObjectId id: unsyncedPotatoDeletions) { Log.w("EXAMPLE", "Deleting " + unsyncedPotatoDeletions.size() + " potato objects."); newRealm.executeTransaction(transactionRealm -> { transactionRealm.where(Potato.class).equalTo("_id", id) .findAll().deleteAllFromRealm(); }); } for(ObjectId id: unsyncedOnionDeletions) { Log.w("EXAMPLE", "Deleting " + unsyncedOnionDeletions.size() + " onion objects."); newRealm.executeTransaction(transactionRealm -> { transactionRealm.where(Onion.class).equalTo("_id", id) .findAll().deleteAllFromRealm(); }); } for(ObjectId id: unsyncedRiceDeletions) { Log.w("EXAMPLE", "Deleting " + unsyncedRiceDeletions.size() + " rice objects."); newRealm.executeTransaction(transactionRealm -> { transactionRealm.where(Rice.class).equalTo("_id", id) .findAll().deleteAllFromRealm(); }); } // Output the state of the freshly downloaded realm, after recovering local data. Log.v("EXAMPLE", "Number of potato objects in the new realm: " + newRealm.where(Potato.class).findAll().size()); Log.v("EXAMPLE", "Number of onion objects in the new realm: " + newRealm.where(Onion.class).findAll().size()); Log.v("EXAMPLE", "Number of rice objects in the new realm: " + newRealm.where(Rice.class).findAll().size()); // close the realms backupRealm.close(); newRealm.close(); }); // execute the recovery logic on a background thread try { executor.awaitTermination(20000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } }
fun handleManualReset(app: App, session: SyncSession?, error: ClientResetRequiredError) { Log.w("EXAMPLE", "Beginning manual reset recovery.") // Close all instances of the realm -- this application only uses one globalRealm!!.close() try { Log.w("EXAMPLE", "About to execute the client reset.") // Move the realm to a backup file: execute the client reset error.executeClientReset() Log.w("EXAMPLE", "Executed the client reset.") } catch (e: IllegalStateException) { Log.e("EXAMPLE", "Failed to execute the client reset: " + e.message) // The client reset can only proceed if there are no open realms. // if execution failed, ask the user to restart the app, and we'll client reset // when we first open the app connection. val restartDialog = AlertDialog.Builder(activity) .setMessage("Sync error. Restart the application to resume sync.") .setTitle("Restart to Continue") .create() restartDialog.show() } // Open new instance of the realm. This initializes a new file for the new realm // and downloads the backend state. Do this in a background thread so we can wait // for server changes to fully download. val executor = Executors.newSingleThreadExecutor() executor.execute { val newRealm = Realm.getInstance(globalConfig) // Download all realm data from the backend -- ensure that the backend state is // fully downloaded before proceeding try { app.sync.getSession(globalConfig) .downloadAllServerChanges(10000, TimeUnit.MILLISECONDS) } catch (e: InterruptedException) { e.printStackTrace() } Log.w("EXAMPLE", "Opened a fresh instance of the realm.") // Open the the realm backup -- as a dynamic realm // (no formal schema; access all data through field lookups) val backupRealm = DynamicRealm.getInstance(error.backupRealmConfiguration) Log.w("EXAMPLE", "Opened the backup realm.") // To only migrate unsynced data, // you'll need to know the last time the realm synced. // you can keep track of successful sync connections // locally in an object in the realm val lastSuccessfulSynced = backupRealm.where("LastSynced").findFirst() val lastSuccessfulSyncTime = lastSuccessfulSynced!!.getLong("timestamp") // Migrate unsynced changes: move data from the backup // instance of the realm to the new "fresh" instance fetched from the backend. // This includes: // - copying any objects that updated, but didn't sync from the // backup realm to the new realm. // - re-deleting any objects that were deleted locally while we were offline // Insert any unsynced updated objects to the new realm // NOTE: this will overwrite any changes made by other clients // to those objects since the last sync. // Applications that require finer-grained conflict resolution // should use custom logic instead. // This example keeps track of when the object last updated by also writing // to a "lastUpdated" field on write operations. val potatoQuery = backupRealm.where("Potato") .greaterThan("lastUpdated", lastSuccessfulSyncTime) val onionQuery = backupRealm.where("Onion") .greaterThan("lastUpdated", lastSuccessfulSyncTime) val riceQuery = backupRealm.where("Rice") .greaterThan("lastUpdated", lastSuccessfulSyncTime) // insert the backup version of all unsynced object updates + creates into the new realm // NOTE: this process will overwrite writes from other clients, potentially overwriting // data in fields not modified in the backup realm. Use with caution. If this does not // meet your application's needs, consider keeping track of the last write for each // field individually (and recovering them individually, per-field). for (potato in potatoQuery.findAll()) { Log.w("EXAMPLE", "Inserting: " + potato.getString("species")) newRealm.executeTransaction { transactionRealm: Realm -> transactionRealm.insertOrUpdate( Potato(potato) ) } } for (onion in onionQuery.findAll()) { Log.w("EXAMPLE", "Inserting: " + onion.getString("varietal")) newRealm.executeTransaction { transactionRealm: Realm -> transactionRealm.insertOrUpdate( Onion(onion) ) } } for (rice in riceQuery.findAll()) { Log.w("EXAMPLE", "Inserting: " + rice.getString("style")) newRealm.executeTransaction { transactionRealm: Realm -> transactionRealm.insertOrUpdate( Rice(rice) ) } } // re-delete unsynced deletions from the new realm // caveat: if an object has been updated SINCE the last update from this client, // (from another client) this does not delete that object. This doesn't match // realm's usual "deletes always win" behavior but it isn't possible to // distinguish between: // - objects that were deleted from this client after the last sync // - objects that were created by another client after the last sync // So instead of deleting innocent objects created by other clients, we let // other client updates "win" in this case. // This means that previously deleted (but unsynced) objects could reappear on this // client after the client reset event. // get all the ids of objects that haven't been updated since the last client sync // (anything that's been updated since the last sync should not be deleted) // -- could be new object, or an object this client deleted but another client modified val allNewPotatoIds = newRealm.where( Potato::class.java ) .lessThan("lastUpdated", lastSuccessfulSyncTime) .findAll().stream() .map { obj: Potato -> obj._id } .collect(Collectors.toSet()) val allNewOnionIds = newRealm.where( Onion::class.java ) .lessThan("lastUpdated", lastSuccessfulSyncTime) .findAll().stream() .map { obj: Onion -> obj._id } .collect(Collectors.toSet()) val allNewRiceIds = newRealm.where( Rice::class.java ) .lessThan("lastUpdated", lastSuccessfulSyncTime) .findAll().stream() .map { obj: Rice -> obj._id } .collect(Collectors.toSet()) Log.v( "EXAMPLE", "number of potatoes in fresh realm" + "that have not been updated since last sync: " + allNewPotatoIds.size ) Log.v( "EXAMPLE", "number of onions in fresh realm" + "that have not been updated since last sync: " + allNewOnionIds.size ) Log.v( "EXAMPLE", "number of rices in fresh realm" + "that have not been updated since last sync: " + allNewRiceIds.size ) // get all the ids of objects in the backup realm val allOldPotatoIds = backupRealm.where("Potato") .findAll().stream() .map { obj: DynamicRealmObject -> obj.getObjectId( "_id" ) } .collect(Collectors.toSet()) val allOldOnionIds = backupRealm.where("Onion") .findAll().stream() .map { obj: DynamicRealmObject -> obj.getObjectId( "_id" ) } .collect(Collectors.toSet()) val allOldRiceIds = backupRealm.where("Rice") .findAll().stream() .map { obj: DynamicRealmObject -> obj.getObjectId( "_id" ) } .collect(Collectors.toSet()) Log.v("EXAMPLE", "number of potatoes in the backup realm: " + allOldPotatoIds.size) Log.v("EXAMPLE", "number of onions in the backup realm: " + allOldOnionIds.size) Log.v("EXAMPLE", "number of rices in the backup realm: " + allOldRiceIds.size) // Get the set of: // all objects in the new realm // - that have not been updated since last sync // - that are not in the backup realm // Those objects were deleted from the backup realm sometime after the last sync. val unsyncedPotatoDeletions = allNewPotatoIds.stream() .filter(Predicate { o: ObjectId -> allOldPotatoIds.contains(o) }.negate()) .collect(Collectors.toSet()) val unsyncedOnionDeletions = allNewOnionIds.stream() .filter(Predicate { o: ObjectId -> allOldOnionIds.contains(o) }.negate()) .collect(Collectors.toSet()) val unsyncedRiceDeletions = allNewRiceIds.stream() .filter(Predicate { o: ObjectId -> allOldRiceIds.contains(o) }.negate()) .collect(Collectors.toSet()) Log.v("EXAMPLE", "Number of potatos to re-delete: " + unsyncedPotatoDeletions.size) Log.v("EXAMPLE", "Number of onions to re-delete: " + unsyncedOnionDeletions.size) Log.v("EXAMPLE", "Number of rices to re-delete: " + unsyncedRiceDeletions.size) // perform "re-deletions" for (id in unsyncedPotatoDeletions) { Log.w( "EXAMPLE", "Deleting " + unsyncedPotatoDeletions.size + " potato objects." ) newRealm.executeTransaction { transactionRealm: Realm -> transactionRealm.where( Potato::class.java ).equalTo("_id", id).findAll().deleteAllFromRealm() } } for (id in unsyncedOnionDeletions) { Log.w( "EXAMPLE", "Deleting " + unsyncedOnionDeletions.size + " onion objects." ) newRealm.executeTransaction { transactionRealm: Realm -> transactionRealm.where( Onion::class.java ).equalTo("_id", id).findAll().deleteAllFromRealm() } } for (id in unsyncedRiceDeletions) { Log.w( "EXAMPLE", "Deleting " + unsyncedRiceDeletions.size + " rice objects." ) newRealm.executeTransaction { transactionRealm: Realm -> transactionRealm.where( Rice::class.java ).equalTo("_id", id).findAll().deleteAllFromRealm() } } // Output the state of the freshly downloaded realm, after recovering local data. Log.v( "EXAMPLE", "Number of potato objects in the new realm: " + newRealm.where( Potato::class.java ).findAll().size ) Log.v( "EXAMPLE", "Number of onion objects in the new realm: " + newRealm.where( Onion::class.java ).findAll().size ) Log.v( "EXAMPLE", "Number of rice objects in the new realm: " + newRealm.where( Rice::class.java ).findAll().size ) // close the realms backupRealm.close() newRealm.close() } // execute the recovery logic on a background thread try { executor.awaitTermination(20000, TimeUnit.MILLISECONDS) } catch (e: InterruptedException) { e.printStackTrace() } }
참고
이 예제는 간단합니다.
이 예제에서는 각 객체 가 마지막으로 업데이트된 시간을 추적합니다. 결과적으로 백업 영역의 마지막 동기화에 성공한 후 필드가 업데이트된 경우 복구 작업은 새 영역의 전체 객체를 덮어씁니다. 이로 인해 다른 클라이언트가 업데이트한 필드를 이 클라이언트의 이전 데이터로 덮어쓸 수 있습니다. Realm 객체에 중요한 데이터가 포함된 필드가 여러 개 포함된 경우 대신 각 필드 의 마지막 업데이트 시간을 추적하고 각 필드를 개별적으로 복구하는 것이 좋습니다.
대체 구현
다른 가능한 구현 사항은 다음과 같습니다.
전체 백엔드를 백업 상태로 덮어씁니다: '마지막 업데이트 시간' 또는 '마지막 동기화 시간' 없이 백업 영역의 모든 객체를 새 영역으로
insertOrUpdate()
합니다. 이 접근 방식으로는 동기화되지 않은 삭제를 복구할 방법이 없습니다. 이 접근 방식은 마지막 동기화 이후 다른 클라이언트가 백엔드에 기록된 모든 데이터를 덮어씁니다. 각 영역에 한 명의 사용자만 쓰는 애플리케이션에 권장됩니다.필드별 변경 사항 추적: 모든 객체 의 '마지막 업데이트 시간'을 추적하는 대신 모든 필드 의 '마지막 업데이트 시간'을 추적합니다. 다른 클라이언트의 필드 쓰기를 이전 데이터로 덮어쓰지 않으려면 이 로직을 사용하여 필드를 개별적으로 업데이트합니다. 필드 수준에서 충돌을 해결해야 하는 객체당 필드가 많은 애플리케이션에 권장됩니다.
객체와 별도로 업데이트 추적 : 각 객체의 스키마에서 '마지막 업데이트 시간'을 추적하는 대신 스키마에
Updates
라는 다른 모델을 만듭니다.Updates
제외) 객체의 필드가 업데이트될 때마다 기본 키, 필드 및 업데이트 시간을 기록합니다. 클라이언트를 재설정하는 동안 백업 Realm에서 해당 필드의 최신 값을 사용하여 "마지막 동기화 시간" 이후에 발생한 모든Update
이벤트를 "다시 작성"합니다. 이 접근 방식은 오래된 데이터로 필드를 덮어쓰지 않고 새 영역에서 동기화되지 않은 모든 로컬 변경 사항을 복제해야 합니다. 그러나 애플리케이션이 자주 작성하는 경우 업데이트 collection을 저장하는 데 비용이 많이 들 수 있습니다. 객체 모델에 'lastUpdated' 필드를 추가하는 것이 바람직하지 않은 애플리케이션에 권장됩니다.