React Nativeにおけるローカルデータベースの考察

前回の同期の話を考えたときに、どうのようにクライアントのローカルにデータを保存するのかという問題が出てきます。

私が調べた限り、その方法は下記のようになります。

  1. ReduxのStoreで管理 — on Memory
  2. AsyncStorageを直接扱う — on Storage
  3. AsyncStorageのラッパー(react-native-store)を使う — on Storage
  4. Native(sqlite)のデータベース(react-native-sqlite-storage)を使う — on Memory
  5. Javascript database(NeDB – react-native-local-mongodb)を使う — on Memory

当たり前ですが、on Memroyは超高速ですが、容量を気にする必要があります。

1. ReduxのStoreで管理

ReduxのStoreをredux-persistなどを使いAsyncStorageにダンプすること前提です。
いい意味でも悪い意味でも自分で管理する必要が出てきます。具体的には、

  • Storeに保存するデータの構造を自前で考える必要がある
  • データを取り出すFind、整列のsortのようなメソッドを自前で作る必要がある。
  • つまり、自分でデータ構造を変えたときに、すべて書きなおすことになり修羅の道。

データ量が少なく単純な構造のときはReduxのStoreで管理しても問題ないでしょう。

2. AsyncStorageを直接扱う

オフィシャルのDocumentでも直に使うのは推奨されてません。

It is recommended that you use an abstraction on top of AsyncStorage instead of AsyncStorage directly for anything more than light usage since it operates globally.

問題点は “ReduxのStoreで管理”と同様に自前で作ることですが、それに増してStorageを扱うので処理が遅いことです。この方法にすることは基本的にないでしょう。

3. AsyncStorageのラッパーを使う

react-native-store以外にもいくつか同じようなモジュールを見かけました。Findのような関数が付属するので自前でメソッドを書く必要がありません。唯一の欠点は、速度だと思います。react-native-storeのgithub issueでもperformanceが問題になっていました。データ量が増えるとパフォーマンス的に結構厳しそうです。ただAsyncStorageのAPIをプラットフォームが提供すれば、どこでも使えることは選択する上で大事なポイントです。

4. Native(sqlite)のデータベースを使う

iOSやAndroidはsqliteのデータベースが付属しています。このsqliteをブリッジするモジュールを使います。sqliteはメモリ上で動くので高速ですし、メソッド等すべてネイティブですし、最速だと思います。
調べてみると、react-native-sqlite-storageというものがあり、CordovaのCordova-sqlite-storageのReact Nativeポートです。これは、iOSとAndroid両方同じAPIでSQLiteを叩けるものです。

Nativeコードのラッパーなので、将来、react-native-macosやreact-native-web、もしくは他プラットフォームが出てきたときに、コードの再利用が他のプラットフォームでできなくなってしまい、その場でreact-native-sqlite-storageのプラットフォームサポートを待つか自分で抽象クラスみたいなものを実装しなければいけないかもしれません。

プラットフォームのターゲットが決まっている場合は、これを選択すべきでしょう。Nativeの利点(メモリ管理など)を思う存分享受できるでしょう。

5. Javascript databaseを使う

nodeの世界には、javascriptで実装されたdatabaseというものがあります。例えば、MongoDBの簡略版のNeDBです。React Nativeにはreact-native-local-mongodbというNodeモジュールからポートされたモジュールがあります。On Memoryで動き、AsyncStorageにPersistすることもできます。Redisみたなものと想像すれば良いでしょうか。逐次AsyncStorageに書き出すのではなく、あるタイミングで書き出します。したがって、データを失う可能性がないとは言えません。

流用性と速度を気にしている場合は選択肢に入ると思いますが、データロスを気にするアプリやメモリの量を気にする場合は避けた方がいいかもしれせん。

最後に

用途によって使い分けましょう:)

(モバイル)アプリとサーバ間の同期方法 – Evernote way

アプリとサーバ同期の方法は、自力で生み出すのは試行錯誤するのは目に見えているので既存の方法を調べてみました。あまり確立した方法がない中、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を持つので上記の変数は下記のように変化します。

  1. アカウント作成時に初期値user.updateCount = 0となる
  2. TodoListのタスクが作られると、task1.usn=1, user.updateCount=1
  3. もう一つタスクが作られると、task2.usn=2, user.UpdateCount=2
  4. task1をアップデートして、task1.usn=3, user.updateCount=3
  5. task2を削除してhard deleteでuser.updateCount=4、soft Deleteはtask1.usn=4, user.updateCount=4
  6. Taskの上位概念のProjectを作成project1.usn=5, user.updateCount=5

同期するオブジェクトに番号がふられるので、クライアント側でどの時点のupdateCount(=lastUpdateCount)を記録しておけば、インクリメントで該当オブジェクトを取得できる。

クライアントとサーバ間の同期方法

それでは、同期方法をみていきます。クライアント側からの見方になります。

  1. 1度も同期した事がなければ、Full Sync。
  2. fullSyncBefore > lastSyncTime, の時、 Full Sync。(例えば、なんらかの理由でForceアップデートしたい時にfullSyncBeforeをその変更時間にすれば、強制的にFull Syncさせることができる)
  3. updateCount = lastUpdateCountの時、サーバ側に変更がないので、クライアント側の変更を更新する(=Send Changes)
  4. それ以外のときは、Incremental Syncする。

Full Sync

Full Syncとは全データに対して走査し、同期すること。クライアントのデータを全消しして、全部取得することではないので注意。スペック内のExampleが名前が一意である必要のあるTagを使って説明しているので少しわかりづらい。一意である必要がある場合は、Renameを考慮する必要がある。下記は、Todoのタスクのように一意性を問わない場合の例。

  1. 更新すべきデータを取得する。
  2. IDのリストを作って、クライアントDBと比較し、クライアント側にIDがない場合は、クライアントDBに追加する
  3. IDがクライアントにあって、サーバ側にない場合、
    1. クライアントのオブジェクトのdirtyフラグがない場合、そのデータをクライアントから消す
    2. dirtyフラグがある場合は、あとでアップロードする。
  4. IDがクライアントとサーバ両方にある場合は、オブジェクトのUSNを比較して、
    1. 同じなら、データは同期している
    2. 同じで、クライアント側にdirtyフラグがある場合は、後でアップロードする
    3. サーバ側のUSNが大きくて、dirtyフラグがない場合、サーバ側のデータに置き換える
    4. サーバ側のUSNが大きくて、dirtyフラグがある場合、コンフリクト発生しているのでマージするか、ユーザにコンフリクト状態をレポートする
  5. サーバのデータマージが終わったら、サーバのupdateCountをクライアントのlastUpadteCountにして、現在の時間をlastSyncTimeにする
  6. 変更を更新する(=send Changes)

Incremental Sync

Full Syncと似ているが差分だけとってくるので、各オブジェクトのUSNの比較を行わなくて良い。削除済みリストとクライアントのリストを比較して必要なら削除する必要がある。

  1. クライアントのlastUpadteCount以降のデータを取得する
  2. IDのリストを作って、クライアントDBと比較し、クライアント側にIDがない場合は、クライアントDBに追加する
  3. IDがクライアントとサーバ両方にある場合は、
    1. dirtyフラグがない場合、サーバ側のデータに置き換える
    2. dirtyフラグがある場合、コンフリクト発生しているのでマージするか、ユーザにコンフリクト状態をレポートする
  4. サーバからのデータがから削除済みのリストを作成し、対象データがある場合はクライアント側から削除
  5. サーバ側のデータマージが終わったら、サーバのupdateCountをクライアントのlastUpadteCountにして、現在の時間をlastSyncTimeにする
  6. 変更を更新する(=send Changes)

Send Changes

  1. ローカルにあるdirtyフラグのあるオブジェクトに対して、
    1. 新しいオブジェクト(USNが設定されてない)場合は、サーバ側にCreateをリクエストする。
    2. オブジェクトが変更された(USNが設定されている)場合は、サーバ側にUpdateをリクエストする。
  2. レスポンスとして、
    • USN = lastUpdateCount + 1のときは同期状態。クライアントのlastUpdateCountを更新する
    • USN > lastUpdateCount + 1のときは、同期していないので、後でIncremental Syncをする。
Scroll to top