はじめに
AndroidX RoomのVersion 2.4.0-alpha04で、multimapsという機能が追加されたようです。
One-to-One や One-to-Many の関係があるテーブルをJoinした結果をMapで返すことができるようになるみたいです。面白そうなのでこれを試そうと思います。
なお上記のリリースノートではSong
やArtist
を使ったサンプルコードが乗っていますが、実装の詳細はUnitTest実装を参照すると良さそうです。
環境
次の環境で検証しています。
- Android Studio Bumblebee | 2021.1.1 Canary 12
- Room 2.4.0-alpha05
準備
まずは適当にEntity
を用意します。
One-To-OneとしてUser to Bank、One-To-ManyとしてUser to Paymentとしてみました。
@Entity(tableName = "User") data class UserEntity( @PrimaryKey @ColumnInfo(name = "user_id") val id: String, @ColumnInfo(name = "user_name") val name: String, @ColumnInfo(name = "bank_id") val bankId: String ) @Entity(tableName = "Bank") data class BankEntity( @PrimaryKey @ColumnInfo(name = "bank_id") val id: String, val name: String ) @Entity(tableName = "Payment") data class PaymentEntity( @PrimaryKey @ColumnInfo(name = "payment_id") val id: String, val amount: Int, @ColumnInfo(name = "user_id") val userId: String )
注意①
multimapで返すEntityに同名のカラムが存在すると、結果がおかしくなるバグが存在するようです。
https://issuetracker.google.com/issues/201306012
同名カラムが存在する場合にはワーニングを出力する方向で対応が行われそうです。
issuetrackerにもコメントされていますが、当面は、同名カラムにエイリアスをつけるなどして回避するのが良さそうです。
この記事では@ColumnInfo
で重複しないカラム名を設定しています。
注意②
Entityにdata class
を利用しないと、次のビルドエラーが発生します(Javaの場合には警告内容どおりにequalsとhashCodeを実装すればよさそうです)。
警告:The key of the provided method's multimap return type must implement equals() and hashCode().
肝心のmultimapのDaoメソッドはこんな感じにしてみました。
@Query("SELECT * FROM User JOIN Bank ON User.bank_id = Bank.bank_id") suspend fun getUserAndBank(): Map<UserEntity, BankEntity> @Query("SELECT * FROM User JOIN Payment ON User.user_id = Payment.user_id") suspend fun getUserAndPayments(): Map<UserEntity, List<PaymentEntity>>
suspendを使うためandroidx.room:room-ktx
をdependenciesに追加しています。
遊ぶ
UnitTestを書いて遊んでみます。
One-To-One(User to Bank)はこんな感じ。
// before val bank1 = BankEntity("bank_id_1", "AAA") val bank2 = BankEntity("bank_id_2", "BBB") val banks = listOf(bank1, bank2) val john = UserEntity("user_id_1", "John", "bank_id_1") val bob = UserEntity("user_id_2", "Bob", "bank_id_2") val andy = UserEntity("user_id_3", "Andy", "bank_id_2") val users = listOf(john, bob, andy) // during bankDao.insertBanks(banks) userDao.insertUsers(users) // after val userToBank = userDao.getUserAndBank() val expected = mapOf( john to bank1, bob to bank2, andy to bank2 ) assertThat(userToBank).containsExactlyEntriesIn(expected)
One-To-Many(User to Payment)はこんな感じです。
// after val bank1 = BankEntity("bank_id_1", "AAA") val bank2 = BankEntity("bank_id_2", "BBB") val banks = listOf(bank1, bank2) val john = UserEntity("user_id_1", "John", "bank_id_1") val bob = UserEntity("user_id_2", "Bob", "bank_id_2") val andy = UserEntity("user_id_3", "Andy", "bank_id_2") val users = listOf(john, bob, andy) val payment1 = PaymentEntity("payment_id_1", 100, "user_id_1") val payment2 = PaymentEntity("payment_id_2", 100, "user_id_2") val payment3 = PaymentEntity("payment_id_3", 200, "user_id_2") val payment4 = PaymentEntity("payment_id_4", 100, "user_id_3") val payment5 = PaymentEntity("payment_id_5", 200, "user_id_3") val payments = listOf(payment1, payment2, payment3, payment4, payment5) // during bankDao.insertBanks(banks) userDao.insertUsers(users) paymentDao.insertPayments(payments) // after val userToPayment = userDao.getUserAndPayments() val expected = mapOf( john to listOf(payment1), bob to listOf(payment2, payment3), andy to listOf(payment4, payment5) ) assertThat(userToPayment).containsExactlyEntriesIn(expected)
期待通りです。
さいごに
multimap、使い所はそこそこあるような気がします。JOINしたEntityの情報すべてが欲しい状況で、新たにデータクラスを作成するのではなくてmultimapを使えばいい場面もあるかもしれません。
またRoom 2.4.0-alpha05では MapInfo
というアノテーションが追加されていて、multimapが使える幅が増えているようです。
利用したコードはこちらにおいています。記事中では省略しましたが、EntityにはforeignKeys
を設定し、データ不整合が発生しないようにしています。