この記事はRoomを使ってenum classのSetを保存するconverterを効率良く実装する方法について書いてあります。
ソースコードはこちら
サンプルコード
適当なデータ(HogeEntity
)をRoomのデータベースに保存することにします。HogeEntity
のプロパティであるanimals
とseasons
はenum classのSetです。
@Entity
data class HogeEntity(
@PrimaryKey(autoGenerate = true)
val id: Int,
val animals: Set<Animal>,
val seasons: Set<Season>
)
enum class Animal {
DOG, CAT, BIRD
}
enum class Season {
SPRING, SUMMER, AUTUMN, WINTER
}
DatabaseとDAOを以下のように実装します。
@Database(entities = [HogeEntity::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
abstract fun hogeDao(): HogeDao
companion object {
private const val dbName = "my.db"
fun createDbInstance(context: Context): MyDatabase = Room
.databaseBuilder(context, MyDatabase::class.java, dbName)
.fallbackToDestructiveMigration()
.build()
}
}
@Dao
interface HogeDao {
@Query("SELECT * FROM HogeEntity")
fun findAll(): List<HogeEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsert(hogeEntity: HogeEntity)
}
この状態でビルドすると、
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
というエラーが出ます。これはHogeEntity
のプロパティのanimals
とseasons
の型がサポートされていないために出るエラーで、指示通りにconverterを実装することで解決します。
Converter
enum classのSetのためのconverterを実装する前に、データベースに保存できる型にどのように変換するかを考えます。Setの要素(enum)の一つ一つを2進数のあるビットに紐づけ、Setに含まれているならば1
、そうでないならば0
とすることで、Int
で表現できそうです。
例えば、Set<Animal>
は以下のようにInt
に変換することができます。
Set<Animal> |
Int (2進数表示) |
Int (10進数表示) |
---|---|---|
[] | 0b000 | 0 |
[DOG] | 0b001 | 1 |
[CAT] | 0b010 | 2 |
[CAT, DOG] | 0b011 | 3 |
[BIRD] | 0b100 | 4 |
[BIRD, DOG] | 0b101 | 5 |
[BIRD, CAT] | 0b110 | 6 |
[BIRD, CAT, DOG] | 0b111 | 7 |
実装
方針が決定したので、HogeEntity
のプロパティのanimals
とseasons
の型であるSet<Animal>
とSet<Season>
を変換するconverterを実装していきます。これらの2つのconverterは似たような処理が多いので、Template methodパターンを利用して共通化します。
abstract class EnumSetConverters<T : Enum<T>>(private val clazz: Class<T>) {
protected abstract val T.storeBit: Int
@TypeConverter
fun toDbValue(enums: Set<T>?): Int? = enums?.sumBy { 1 shl it.storeBit }
@TypeConverter
fun fromDbValue(dbValue: Int?): Set<T>? {
dbValue ?: return null
return clazz.enumConstants
?.filter { dbValue ushr it.storeBit and 1 == 1 }
?.toSet()
}
}
AbstractClassであるEnumSetConverters
は共通化したい2つのメソッド(toDbValue()
とfromDbValue()
)と、それぞれのconverterで異なる処理をしたいprotectedな拡張プロパティ(storeBit
)で構成されています。ConcreteClassである2つのconverterの実装は以下のようになります
class AnimalConverters : EnumSetConverters<Animal>(Animal::class.java) {
override val Animal.storeBit: Int
get() = when (this) {
Animal.DOG -> 0
Animal.CAT -> 1
Animal.BIRD -> 2
}
}
class SeasonConverters : EnumSetConverters<Season>(Season::class.java) {
override val Season.storeBit: Int
get() = when (this) {
Season.SPRING -> 0
Season.SUMMER -> 1
Season.AUTUMN -> 2
Season.WINTER -> 3
}
}
このようにTemplate methodパターンを利用することで、新しいenum classのSetのconverterを追加する時には、そのconverter固有の処理であるenumをどのビットに紐づけるかを実装するだけで良くなります。
正しく動くことを確認
作成した2つのconverterをRoomのデータベースに設定し
@Database(entities = [HogeEntity::class], version = 1)
@TypeConverters(AnimalConverters::class, SeasonConverters::class)
abstract class MyDatabase : RoomDatabase() {
...
}
保存した後にロードして確認します。
hogeDao.upsert(
HogeEntity(
id = 0,
animals = setOf(Animal.DOG, Animal.BIRD),
seasons = setOf(Season.SUMMER, Season.WINTER)
)
)
hogeDao.upsert(
HogeEntity(
id = 0,
animals = emptySet(),
seasons = Season.values().toSet()
)
)
Log.d("hoge list", hogeDao.findAll().toString())
ログの出力は以下の通りで、問題なく保存されていることを確認することができました。
D/hoge list: [HogeEntity(id=1, animals=[DOG, BIRD], seasons=[SUMMER, WINTER]), HogeEntity(id=2, animals=[], seasons=[SPRING, SUMMER, AUTUMN, WINTER])]
おわりに
今回はTemplate methodパターンを使ってRoomのconverterを効率よく実装しました。デザインパターンはうまく使うと非常に強力なので積極的に活用していきたいですね。