Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
0
Help us understand the problem. What are the problem?

posted at

updated at

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

この記事は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を効率よく実装しました。デザインパターンはうまく使うと非常に強力なので積極的に活用していきたいですね。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
0
Help us understand the problem. What are the problem?