LoginSignup
1
0

More than 3 years have passed since last update.

[Android] enum classのSetを保存するconverterをTemplate methodパターンで効率よく実装

Last updated at Posted at 2020-12-09

この記事はRoomを使ってenum classのSetを保存するconverterを効率良く実装する方法について書いてあります。
ソースコードはこちら

サンプルコード

適当なデータ(HogeEntity)をRoomのデータベースに保存することにします。HogeEntityのプロパティであるanimalsseasonsは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のプロパティのanimalsseasonsの型がサポートされていないために出るエラーで、指示通りに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のプロパティのanimalsseasonsの型である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を効率よく実装しました。デザインパターンはうまく使うと非常に強力なので積極的に活用していきたいですね。

1
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0