この記事はSmartDrive Advent Calendar 2017 24日目の記事です。
Android Architecture ComponentsのRoomを触る中、テーブル継承を扱う場合に結構特徴が現れるなぁと思ったのでそのあたりを紹介していきます。
はじめに
テーブル継承についてはこの辺を参考にしてください。
http://bliki-ja.github.io/pofeaa/SingleTableInheritance/
http://bliki-ja.github.io/pofeaa/ClassTableInheritance/
http://bliki-ja.github.io/pofeaa/ConcreteTableInheritance/
また、ここでは各種テーブル継承のpros/consのお話はしません。
Roomを使った実装について焦点を当てていきます。
Model
sealed class Data {
abstract val id: String
abstract val name: String
}
data class DataA(override val id: String, override val name: String, val a: String) : Data()
data class DataB(override val id: String, override val name: String, val b: Int) : Data()
このような継承関係にあるModelを単一テーブル継承/クラステーブル継承/具象テーブル継承それぞれのEntityとDaoを書いてみます。
単一テーブル継承
@Entity
class DataEntity(
@PrimaryKey
val id: String,
val name: String,
val a: String?,
val b: Int?
)
@Dao
interface DataDao {
@Insert
fun insert(vararg dataEntity: DataEntity)
@Query("SELECT id, name, a FROM dataEntity WHERE name = :name")
fun findDataAByName(name: String): List<DataA>
@Query("SELECT id, name, b FROM dataEntity WHERE name = :name")
fun findDataBByName(name: String): List<DataB>
}
単一テーブル継承でこのように実装してみました。
ここで注目したいのはModelのNotNullなfieldとDataEntityのNullableなfieldです。
他のORMだと!!
をつけたり無駄にnullチェックをする必要があったりしてなんだかなぁという気持ちになるのですが、RoomではQueryの結果をPOJOにmappingする機能のおかげでNotNullとNullableのミスマッチをあまり意識することなくコードを記述することができます。
クラステーブル継承
@Entity
class DataEntity(
@PrimaryKey
val id: String,
val name: String
)
@Entity(foreignKeys = [(ForeignKey(entity = DataEntity::class, parentColumns = ["id"], childColumns = ["dataId"]))])
class DataAEntity(
@PrimaryKey
val dataId: String,
val a: String
)
@Entity(foreignKeys = [(ForeignKey(entity = DataEntity::class, parentColumns = ["id"], childColumns = ["dataId"]))])
class DataBEntity(
@PrimaryKey
val dataId: String,
val b: Int
)
@Dao
interface DataDao {
@Insert
fun insert(vararg dataEntity: DataEntity)
@Insert
fun insert(vararg dataEntity: DataAEntity)
@Insert
fun insert(vararg dataEntity: DataBEntity)
@Query("SELECT dataEntity.id AS id, dataEntity.name AS name, dataAEntity.a AS a FROM dataEntity, dataAEntity WHERE dataEntity.name = :name AND dataEntity.id = dataAEntity.dataId")
fun findDataAByName(name: String): List<DataA>
@Query("SELECT dataEntity.id AS id, dataEntity.name AS name, dataBEntity.b AS b FROM dataEntity, dataBEntity WHERE dataEntity.name = :name AND dataEntity.id = dataBEntity.dataId")
fun findDataBByName(name: String): List<DataB>
}
クラステーブル継承はこんな感じにしてみました。
ここはあまりおもしろい事ないかなぁという感じです。
RoomではEntity間の関連付けができないので他のORMよりは少しばかり実装者に負担がかかりそうですね。
あと、Query結果のcolumn名をfiledと適合するようにrenameの必要があるのが面倒かなぁ。。。
具象テーブル継承
@Entity(primaryKeys = ["id"])
class DataAEntity(
@Embedded
val data: DataA
)
@Entity(primaryKeys = ["id"])
class DataBEntity(
@Embedded
val data: DataB
)
@Dao
interface DataDao {
@Insert
fun insert(vararg dataEntity: DataAEntity)
@Insert
fun insert(vararg dataEntity: DataBEntity)
@Query("SELECT id, name, a FROM dataAEntity WHERE name = :name")
fun findDataAByName(name: String): List<DataA>
@Query("SELECT id, name, b FROM dataBEntity WHERE name = :name")
fun findDataBByName(name: String): List<DataB>
}
具象テーブル継承はこんな感じにしてみました。
RoomではEmbeddedアノテーションを使うとクラスのfieldをそのままEntityのfiledとして扱うことができます。
すなわちテーブルとサブクラスを直接mappingする具象テーブル継承ではEntityをここまで単純に記述可能になります。
すごい!!
Embeddedを使う際の注意
実はこのEmbeddedには落とし穴があってModelを別moduleに置いてしまうとbuildが通らないという現象が発生します。。。
https://qiita.com/atsuya046/items/758951313fd55f749900
これ結構困るのでなんとかならないものかなぁ
まとめ
- 単一テーブル継承はNotNull-Nullableのミスマッチを意識しなくなる
- クラステーブル継承は他のORMより実装がちょっと面倒かも
- 具象テーブル継承は(制限はあるが)結構いい感じになる
Roomは他のORMと設計思想が違うので特徴的な部分が多くて面白いですね。
引き続きRoomの特徴見つけていこうと思います。
参考
Room Persistence Library | Android Developers
エンタープライズ アプリケーションアーキテクチャパターン (Object Oriented SELECTION)