こんにちは、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によるアプリの改善に取り組んでいければ、と考えています!

