[Android] 032. Room(DB) Relation vs TypeConverter

List<T>やMap<T>はDBの値として直接格納できないのでどのように対応するか的な話です

List<T>やMap<T>をJsonの文字列に変換してStringとして保存する方法と
個別にDBのTableを作ってRelationで連結して取得する方法があると思います

前者は実装が楽だけどデータ構造によってはデータ量が膨れ上がる、後者は実装量が増えてめんどくさいけどDBでよくある方法です

TypeConverterの説明は以下
https://developer.android.com/training/data-storage/room/referencing-data?hl=ja

RoomでRelationのdeveloperの説明は以下
https://developer.android.com/training/data-storage/room/relationships?hl=ja

PokeApiのStat(HP/こうげき…などの名称データ)を例に見ていきます
Statの内容はkeyとなるnameのリストとid/言語ごとのnameのリストなどの詳細データです

TypeConverter

RoomDBを定義時にTypeConverterを指定するだけです
Jsonはkotlinx.serialization.Jsonを使ってます

※ListTypeConverterは使わないけどついでに定義

@Entity(tableName = "stat")
data class Stat(
    @PrimaryKey val id: Int,
    val name: String,
    // language, name
    val names: Map<String, String>,
)
@Dao
interface StatDao {
    @Query("SELECT * FROM stat")
    override fun getAll(): Flow<List<Stat>>
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(list: List<Stat>)
}

class ListTypeConverter {
    @TypeConverter
    fun fromList(list: List<String>): String {
        return Json.encodeToString(list)
    }
    @TypeConverter
    fun toList(str: String): List<String> {
        return Json.decodeFromString<List<String>>(str)
    }
}
class MapTypeConverter {
    @TypeConverter
    fun fromMap(map: Map<String, String>): String {
        return Json.encodeToString(map)
    }
    @TypeConverter
    fun toMap(str: String): Map<String, String> {
        return Json.decodeFromString<Map<String, String>>(str)
    }
}

@Database(entities = [Stat::class], version = 1, exportSchema = false)
@TypeConverters(ListTypeConverter::class, MapTypeConverter::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun dao(): StatDao
}

Relation

@Entity(tableName = "stat_name")
data class StatName(
    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val count: Int = 0,
    @ColumnInfo(name = "name_id") val id: Int,
    val language: String,
    val name: String,
)
@Entity(tableName = "stat")
data class StatEntry(
    @PrimaryKey val id: Int,
    val name: String,
)

data class Stat(
    @Embedded val stat: StatEntry,
    @Relation(
        parentColumn = "id",
        entityColumn = "name_id",
        entity = StatName::class
    )
    val names: List<StatName>,
)

@Dao
interface StatNameDao {
    @Transaction
    @Query("SELECT * FROM stat_name")
    override fun getAll(): Flow<List<StatName>>
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(list: List<StatName>)
}
@Dao
interface StatDao {
    @Query("SELECT * FROM stat")
    override fun getAll(): Flow<List<Stat>>
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(list: List<StatEntry>)
}

@Database(entities = [Stat::class, StatName::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun stadDao(): StatDao
    abstract fun stadNameDao(): StatNameDao
}

Android Studio Giraffe 2022.3.1 Patch 1 built on August 17, 2023