こんにちは、Androidチームの若宮(id:D_R_1009)です。
スタディプラスのAndorid版にて、5月半ばより不具合の発生していた「本棚」機能を7月頭に修正したしました。 ご不便、ご迷惑をおかけしましたこと大変申し訳なく思っております。
「本棚」の不具合においては、2つの問題点がありました。
- Room DB の
allowOnMainThread
指定による、DBファイルの破損問題 - 「本棚」内の並び順と、本棚に追加する「教材」の関係性が密すぎる問題
今回はRoom 2.1より追加された DatabaseView
を活用し、2つ目の問題を解決しましたので、その経験をまとめたいと思います。
DatabaseView
とは
Room 2.1より追加された、複数のTableから1つのクラスを作る仕組みです。
2.0までは、あるTableとTableを1対1で対応させるためには、下記のような方法が必要でした。
@Entity(tableName = "parent") class ParentEntity { @Embedded lateinit var child1 : ChildClassA @Embedded lateinit var child2 : ChildClassB } @Dao interface SampleDao { @Query("SELECT * FROM child_class_a, child_class_b where child_class_a.id = child_class_b.id_class_a") fun getParentEntityList(): LiveData<List<ParentEntity>> }
対し、DataBaseView
ではSQLのInner JoinまたはOuter Joinを利用してEntityを作ることができます。
@DatabaseView( viewName = "parent", value = "SELECT * FROM child_class_a INNER JOIN child_class_b ON child_class_a.id = child_class_b.id_class_a" ) data class ParentEntity( @Embedded val child1: ChildClassA, @Embedded val child2: ChildClassB ) @Dao interface SampleDao { @Query("SELECT * FROM parent") fun getParentEntityList(): LiveData<List<ParentEntity>> }
DatabaseView
により、旧来の方法に比べて下記の点が便利になっていると感じます。
- Inner JoinかOuter Joinかを選ぶことができるので、(View層のために)生成したいクラスのNullableがコントロールしやすくなった
lateinit var
では実行時のエラーによる検知しかなかったが、DatabaseView
ではコンパイル時の検知が可能になっているval
でフィールドを定義できる
今回解決するべき問題
本棚の並べ替えにおいては、教材
と 並び順
の2つの要素を組み合わせていきます。
歴史的な経緯により 教材
はユーザー情報に紐づけられてサーバー上に保存されていますが、 並び順
は端末ローカルにしか存在しません。
このため 教材
の追加/修正/削除時には 教材
と 並び順
の2つのテーブルを更新し、 教材
を並び替えた場合には 並び順
のテーブルのみを更新する必要があります。
また各 教材
には カテゴリー
が紐づけられています。この カテゴリー
は例えば「英語」や「数学」などの 教材
を本棚内で管理するための概念です。
もちろん、本棚内で カテゴリー
の並べ替えを行うことができるため、 カテゴリー
に対応する 並び順
が存在します。
教材の並べ替え | カテゴリーの並べ替え |
---|---|
全体の構成図
アプリはAndroid Architecture Componentsを利用したMVVMアーキテクチャを採用しています。 昨年12月ごろは導入半ばといったところでしたが、最近はほとんどViewModelによるビジネスロジックの切り離しが進んでいます。
DatabaseView
の便利なところ、注意した方が良いところ
DatabaseView
の対象クラスに対して、@Insert
や @Update
することはできません。
必ず、その構成している要素の各Tableに対して更新を行う必要があります。
一方で各Tableに対する更新が、対象クラスへの更新通知となります。
このため 並び順
の更新を行うと、 教材
と 並び順
を組み合わせたクラスへの変更通知となります。結果として LiveData<List<SortedMaterial>>
のようにDBからリストを購読していれば、 並び順
の更新後にUIへ変更を伝えることができます。
教材
は 並び順
の親の関係となるため 並び順
の外部キーとして 教材
を指定しました。
このためInsert時のConflictStrategyに REPLACE
を指定してしまうと、 教材
の更新をするたびに 並び順
が破棄されてしまうようになります。
DatabaseView
では外部キー制約を考えることも多くなると思われるので、あらかじめ @Insert
と @Update
を使い分けておくのが良さそうです。
設計上の工夫点
- View層にはDB層の結果だけを表示させる
- RecyclerViewに渡すListはDaoから取得するListに限定
- DBの
並べ替え
テーブルの更新結果をView層が受け取り、Groupieのupdateメソッドによる並べ替えを行う - 各種のソート処理はView層で実行させない
- ViewModel層でView層とDB層(リポジトリ層)との非同期処理の調整を実施
- DB層はKotlin CoroutinesによるDBアクセス、View層はLiveDataによるUI更新となるため、操作対象となるListはViewModelがハンドリング
LiveData.getValue()
によりCachedList
を取得、View層から取得したPositionより並べ替えされた順序リストを作成
- DB層は
@Update
メソッドにより並び順
テーブルのみを更新- Roomの操作をKotlin Coroutinesにより実施させる
@Update
メソッドの処理はArchitecture Components の I/O Dispatcherで実施されるため、呼び出し側でいじらない- こちらを参考にしました Google Developers Japan: Room コルーチン
- 余談ですが
@Transaction
の利用時にはDispatchers.IO
を呼び出し側で指定する必要があります
- 適切に
Index
を貼ることで、 DBから教材
を取得する速度を担保する
まとめ
Room 2.1の DatabaseView
と LiveData
、 RecyclerView
を組み合わせることによる並べ替え機能について紹介しました。
結果として冒頭であげた1つ目の問題、 UIスレッド上でDB内の並べ替えを(長時間)行う
ことによるローカルDBの破損問題にも対応することができ、アプリの安定性に寄与できたかなと思います。
引き続きRoomによるアプリの改善に取り組んでいければ、と考えています!