表題のエラーに直面して解決まで辿り着いたので共有します。
環境
Android Studio Hedgehog | 2023.1.1 Patch 2
経緯
Androidアプリの開発の中で、Roomを実装中にこのエラーに直面しました。
クラッシュした箇所
suspend fun operate() {
getCityCodeJsonDataSource.fetchResponseAsFlow().collect { list ->
list.forEachIndexed { i, data ->
val prefecture = Prefecture(
i,
data.prefectureCode!!,
data.prefectureName!!
)
//logをはくメソッド
footprint("prefecture: $prefecture")
MainApplication().appDatabase.prefectureAndCityDao().upsertPrefecture(prefecture)
val cities = data.municipals
cities?.forEachIndexed { j, city ->
val c = City(
j,
city.code,
city.name,
city.kana,
city.isDisabled,
i
)
//logをはくメソッド
footprint("city: $c")
//***ここでクラッシュ(android.database.sqlite.SQLiteConstraintException...)***
MainApplication().appDatabase.prefectureAndCityDao().upsertCity(c)
}
}
}
原因
端的にいうと「『親』の準備が完了する前に『子』のデータを入れるな!」ということのようです。
実は下記のように2つのモデルは親子関係にあります。
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import androidx.room.Relation
@Entity(tableName = "prefecture")
data class Prefecture(
@PrimaryKey(autoGenerate = true)
val id: Int,
@ColumnInfo(name = "prefecture_name")
val prefectureName: String,
@ColumnInfo(name = "prefecture_code")
val prefectureCode: String,
)
@Entity(
tableName = "city",
foreignKeys = [ForeignKey(
entity = Prefecture::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("p_id"),
onDelete = ForeignKey.CASCADE
)]
)
data class City(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "c_id")
val cId: Int,
val code: String?,
val name: String?,
@ColumnInfo(name = "name_kana")
val nameKana: String?,
@ColumnInfo(name = "prefecture_code")
val isDisabled: Boolean?,
@ColumnInfo(name = "p_id")
val pId: Int
)
data class PrefectureAndCity(
@Embedded
val prefecture: Prefecture,
@Relation(
parentColumn = "id",
entityColumn = "p_id"
)
val cities: List<City>
)
解法
以上のことを踏まえると、データを入れる順番に問題があったことになるので、下記のように親(Prefecture)のデータを全て入れ終わった後に子(City)を入れてあげると上手くいきます。
suspend fun operate() {
getCityCodeJsonDataSource.fetchResponseAsFlow().collect { list ->
list.forEachIndexed { i, data ->
val prefecture = Prefecture(
i + 1, data.prefectureCode!!, data.prefectureName!!
)
footprint("prefecture: $prefecture")
MainApplication().appDatabase.prefectureAndCityDao().upsertPrefecture(prefecture)
}
list.forEachIndexed { i, data ->
val cities = data.municipals
cities?.forEachIndexed { j, city ->
val c = City(
j + 1,
city.code,
city.name,
city.kana,
city.isDisabled,
i + 1
)
footprint("city: $c")
MainApplication().appDatabase.prefectureAndCityDao().upsertCity(c)
}
}
}
}