アプリとサーバ同期の方法は、自力で生み出すのは試行錯誤するのは目に見えているので既存の方法を調べてみました。あまり確立した方法がない中、Evernoteが同期のスペック(EDAM synchronization)を公開していることを知り、そのスペックを読んでみました。
簡単に言うと、サーバ側のデータを中心として、複数クライアントからでも同期できるという方法です。クライアント側でがんばる方法とも言えます。
同期を構成する要素
- Full SyncとIncremental Syncの2パターンがある。
- 同期するオブジェクトはシークエンス番号(Update Squence Number: USN)をもつ。Create/Update/Deleteの度にインクリメントしてアサインする。
- 同期するオブジェクトはGUIDという一意のIDをもつ。
- サーバ側の変数は、
- updateCount: 最大USN
- fullSyncBefore: 削除済みリストをクライアントキャッシュから削除したいときに使うか。または、問題があった場合に、Force FullSyncさせるためのもの。時間型変数(DateTime)。
- クライアント側の変数は、
- lastUpdateCount: 最後に同期したときのサーバから得たupdateCount
- lastSyncTime: 最後に同期した時間型変数(DateTime)
- 各同期オブジェクトにdirtyフラグをもつ。変更があった場合に、同期対象にするためのフラグ。
では、具体例(TodoList)を用いて説明します。ユーザ毎にUpdateCountを持つので上記の変数は下記のように変化します。
- アカウント作成時に初期値
user.updateCount = 0
となる - TodoListのタスクが作られると、
task1.usn=1, user.updateCount=1
- もう一つタスクが作られると、
task2.usn=2, user.UpdateCount=2
- task1をアップデートして、
task1.usn=3, user.updateCount=3
- task2を削除してhard deleteで
user.updateCount=4
、soft Deleteはtask1.usn=4, user.updateCount=4
- Taskの上位概念のProjectを作成
project1.usn=5, user.updateCount=5
- …
同期するオブジェクトに番号がふられるので、クライアント側でどの時点のupdateCount(=lastUpdateCount)を記録しておけば、インクリメントで該当オブジェクトを取得できる。
クライアントとサーバ間の同期方法
それでは、同期方法をみていきます。クライアント側からの見方になります。
- 1度も同期した事がなければ、Full Sync。
fullSyncBefore > lastSyncTime
, の時、 Full Sync。(例えば、なんらかの理由でForceアップデートしたい時にfullSyncBeforeをその変更時間にすれば、強制的にFull Syncさせることができる)updateCount = lastUpdateCount
の時、サーバ側に変更がないので、クライアント側の変更を更新する(=Send Changes)- それ以外のときは、Incremental Syncする。
Full Sync
Full Syncとは全データに対して走査し、同期すること。クライアントのデータを全消しして、全部取得することではないので注意。スペック内のExampleが名前が一意である必要のあるTagを使って説明しているので少しわかりづらい。一意である必要がある場合は、Renameを考慮する必要がある。下記は、Todoのタスクのように一意性を問わない場合の例。
- 更新すべきデータを取得する。
- IDのリストを作って、クライアントDBと比較し、クライアント側にIDがない場合は、クライアントDBに追加する
- IDがクライアントにあって、サーバ側にない場合、
- クライアントのオブジェクトのdirtyフラグがない場合、そのデータをクライアントから消す
- dirtyフラグがある場合は、あとでアップロードする。
- IDがクライアントとサーバ両方にある場合は、オブジェクトのUSNを比較して、
- 同じなら、データは同期している
- 同じで、クライアント側にdirtyフラグがある場合は、後でアップロードする
- サーバ側のUSNが大きくて、dirtyフラグがない場合、サーバ側のデータに置き換える
- サーバ側のUSNが大きくて、dirtyフラグがある場合、コンフリクト発生しているのでマージするか、ユーザにコンフリクト状態をレポートする
- サーバのデータマージが終わったら、サーバのupdateCountをクライアントのlastUpadteCountにして、現在の時間をlastSyncTimeにする
- 変更を更新する(=send Changes)
Incremental Sync
Full Syncと似ているが差分だけとってくるので、各オブジェクトのUSNの比較を行わなくて良い。削除済みリストとクライアントのリストを比較して必要なら削除する必要がある。
- クライアントのlastUpadteCount以降のデータを取得する
- IDのリストを作って、クライアントDBと比較し、クライアント側にIDがない場合は、クライアントDBに追加する
- IDがクライアントとサーバ両方にある場合は、
- dirtyフラグがない場合、サーバ側のデータに置き換える
- dirtyフラグがある場合、コンフリクト発生しているのでマージするか、ユーザにコンフリクト状態をレポートする
- サーバからのデータがから削除済みのリストを作成し、対象データがある場合はクライアント側から削除
- サーバ側のデータマージが終わったら、サーバのupdateCountをクライアントのlastUpadteCountにして、現在の時間をlastSyncTimeにする
- 変更を更新する(=send Changes)
Send Changes
- ローカルにあるdirtyフラグのあるオブジェクトに対して、
- 新しいオブジェクト(USNが設定されてない)場合は、サーバ側にCreateをリクエストする。
- オブジェクトが変更された(USNが設定されている)場合は、サーバ側にUpdateをリクエストする。
- レスポンスとして、
USN = lastUpdateCount + 1
のときは同期状態。クライアントのlastUpdateCountを更新するUSN > lastUpdateCount + 1
のときは、同期していないので、後でIncremental Syncをする。