Android: Roomにおけるデータ変更通知の仕組みについて(InvalidationTracker)
Created at Sun, Nov 26, 2017こんにちはʕ•ӫ̫͡•ʔ
この記事ではどのようにしてRoomがテーブルの変更を検知し、LiveDataなどからデータの変更をオブザーバーに通知しているかについて説明します。
前置き/前提知識
RoomはGoogleが提供しているSQLiteをいい感じに扱うためのライブラリです。
RoomにはDao(Data access object)と呼ばれるインターフェース/抽象クラスがあり、そこを起点にクエリを実行します。
@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getUsers(): Flowable<List<User>>
}
返り値として、observableデータ型を取ることができ、RoomではLiveDataとRxJava2の型(Flowableとか)などのobservable型サポートしています。
そして、LiveDataまたはFlowableで返り値を定義すると対象のテーブルがInsertなどで変更されたときに、変更通知を受け取ることが出来ます。
// クライアント側のコード
userDao.getUsers()
  .subscribeOn(Schedulers.io)
  .subscribe {
     println(it) // ここが対象のテーブルが変更されるたびに評価される。
  }
テーブルの変更検知はRoomの内部クラスのInvalidationTrackerクラスで行っています。
これからInvalidationTrackerの内部実装を見ていきます。
InvalidationTrackerの流れ
- InvalidationTracker#startTrackingTableメソッドでTrigger(SQL)を生成
 - obsevableが生成されたら、対応するInvalidationTracker.Observer(変更通知用コールバック)をInvalidationTrackerに登録
 - RoomData#endTransactionのタイミングで各テーブルが更新されたかを確認
 - テーブルが更新されていたら変更通知コールバックを実行
 
というのが大まかな流れになります。2はobserver(変更通知用コールバック)を登録するだけ、4は登録したコールバックにデータを流すだけです。なので1, 3について詳しく見ていきます。
InvalidationTracker#startTrackingTableメソッドによりTriggerを生成する
最初に各テーブルのバージョンを管理する room_table_modification_logが生成されます。
CREATE TEMP TABLE room_table_modification_log (
    version INTEGER PRIMARY KEY AUTOINCREMENT,
    table_id INTEGER
)
このクエリの意味することは、テーブル操作があったときに、対象のテーブルのversionを更新するようにすることで、version変更されていたら更新を検知することが出来ます。このversionを更新するためにRoomではSQLにある機能の1つTriggerを使っています。具体的には次のTriggerを利用しています。
CREATE TEMP TRIGGER IF NOT EXISTS room_table_modification_trigger_users_UPDATE AFTER UPDATE ON users
BEGIN
    INSERT OR REPLACE INTO room_table_modification_log VALUES (null, 0); # 0はtable_id
END
これは一見複雑に見えますがものすごい単純で、usersテーブルがUPDATEされたら上記のクエリがフックされて実行されるだけです。上のクエリINSERT OR REPLACE INTO room_table_modification_log VALUES (null, 0); は room_table_modification_logテーブルに最新のid + 1のrowを追加/更新するものになります。
上のTriggerはUPDATEですが、その他にDELETE、INSERT用のTriggerも実行されます。これらのTriggerによりテーブルの変更(Update, Insert, Delete)を検知し、前回のversionを新しいものに更新します。
RoomData#endTransactionのタイミングで各テーブルが更新されたかを確認する
Triggerによりversionを更新できたので、次にその更新したテーブルを前回の状態と比較し、差分があるなら発火してあげる必要があります。それは RoomData#endTransactionのタイミングで行っています。
endTransactionのタイミングでInvalidationTracker#refreshVersionsAsyncをコールし、room_table_modification_logテーブルに変更があったら、InvalidationTrackerに登録にしたInvalidationTracker.Observerに対してデータ変更のコールバックをします。これにより生成されたobservable(FlowableとかLiveDataとかとか)に対して変更通知をすることが出来ます。
これがRoomにおけるおおまかな変更検知/通知の仕組みになります。
まとめ
- SQLのTriggerを使っているところが個人的には意外というか、こんな実装があるんだと感心した
 - 上手く説明できた気がしないので、より詳細はInvalidationTrackerの実装を読んで下さい(゜レ゜)