Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

DatabaseView(Room 2.1)による本棚並べ替え機能リリースについて

こんにちは、Androidチームの若宮(id:D_R_1009)です。

スタディプラスのAndorid版にて、5月半ばより不具合の発生していた「本棚」機能を7月頭に修正したしました。 ご不便、ご迷惑をおかけしましたこと大変申し訳なく思っております。

「本棚」の不具合においては、2つの問題点がありました。

  1. Room DB の allowOnMainThread 指定による、DBファイルの破損問題
  2. 「本棚」内の並び順と、本棚に追加する「教材」の関係性が密すぎる問題

今回はRoom 2.1より追加された DatabaseView を活用し、2つ目の問題を解決しましたので、その経験をまとめたいと思います。

developer.android.com

DatabaseView とは

Room 2.1より追加された、複数のTableから1つのクラスを作る仕組みです。

docs.google.com

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>>
}

stackoverflow.com

対し、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 により、旧来の方法に比べて下記の点が便利になっていると感じます。

  1. Inner JoinかOuter Joinかを選ぶことができるので、(View層のために)生成したいクラスのNullableがコントロールしやすくなった
  2. lateinit var では実行時のエラーによる検知しかなかったが、DatabaseView ではコンパイル時の検知が可能になっている
  3. val でフィールドを定義できる

今回解決するべき問題

本棚の並べ替えにおいては、教材並び順 の2つの要素を組み合わせていきます。

歴史的な経緯により 教材 はユーザー情報に紐づけられてサーバー上に保存されていますが、 並び順 は端末ローカルにしか存在しません。 このため 教材 の追加/修正/削除時には 教材並び順 の2つのテーブルを更新し、 教材 を並び替えた場合には 並び順 のテーブルのみを更新する必要があります。

また各 教材 には カテゴリー が紐づけられています。この カテゴリー は例えば「英語」や「数学」などの 教材 を本棚内で管理するための概念です。 もちろん、本棚内で カテゴリー の並べ替えを行うことができるため、 カテゴリー に対応する 並び順 が存在します。

教材の並べ替え カテゴリーの並べ替え
f:id:D_R_1009:20190722181453g:plain f:id:D_R_1009:20190722181519g:plain

全体の構成図

f:id:D_R_1009:20190723153124p:plain

アプリはAndroid Architecture Componentsを利用したMVVMアーキテクチャを採用しています。 昨年12月ごろは導入半ばといったところでしたが、最近はほとんどViewModelによるビジネスロジックの切り離しが進んでいます。

tech.studyplus.co.jp

tech.studyplus.co.jp

DatabaseView の便利なところ、注意した方が良いところ

DatabaseView の対象クラスに対して、@Insert@Update することはできません。 必ず、その構成している要素の各Tableに対して更新を行う必要があります。

一方で各Tableに対する更新が、対象クラスへの更新通知となります。 このため 並び順 の更新を行うと、 教材並び順 を組み合わせたクラスへの変更通知となります。結果として LiveData<List<SortedMaterial>> のようにDBからリストを購読していれば、 並び順 の更新後にUIへ変更を伝えることができます。

教材並び順 の親の関係となるため 並び順 の外部キーとして 教材 を指定しました。 このためInsert時のConflictStrategyに REPLACE を指定してしまうと、 教材 の更新をするたびに 並び順 が破棄されてしまうようになります。 DatabaseView では外部キー制約を考えることも多くなると思われるので、あらかじめ @Insert@Update を使い分けておくのが良さそうです。

設計上の工夫点

  1. View層にはDB層の結果だけを表示させる
    • RecyclerViewに渡すListはDaoから取得するListに限定
    • DBの 並べ替え テーブルの更新結果をView層が受け取り、Groupieのupdateメソッドによる並べ替えを行う
    • 各種のソート処理はView層で実行させない
  2. ViewModel層でView層とDB層(リポジトリ層)との非同期処理の調整を実施
    • DB層はKotlin CoroutinesによるDBアクセス、View層はLiveDataによるUI更新となるため、操作対象となるListはViewModelがハンドリング
    • LiveData.getValue() により CachedList を取得、View層から取得したPositionより並べ替えされた順序リストを作成
  3. DB層は @Update メソッドにより 並び順 テーブルのみを更新
    • Roomの操作をKotlin Coroutinesにより実施させる
    • @Update メソッドの処理はArchitecture Components の I/O Dispatcherで実施されるため、呼び出し側でいじらない
    • 適切に Index を貼ることで、 DBから 教材 を取得する速度を担保する

まとめ

Room 2.1の DatabaseViewLiveDataRecyclerView を組み合わせることによる並べ替え機能について紹介しました。 結果として冒頭であげた1つ目の問題、 UIスレッド上でDB内の並べ替えを(長時間)行う ことによるローカルDBの破損問題にも対応することができ、アプリの安定性に寄与できたかなと思います。

引き続きRoomによるアプリの改善に取り組んでいければ、と考えています!