Android のプロジェクトで Room を使用してハマったことがあったので共有したいと思います。
結論から言うと、ForeignKey
で指定される Entity があった場合、その Entity を Insert する際に OnConflictStrategy.REPLACE
を使用してはいけません。
理由
Dao で @Insert
アノテーションに OnConflictStrategy.REPLACE
を指定すると、SQL 的には INSERT OR REPLACE
文が実行されます。
SQLite の仕様では INSERT OR REPLACE
文では既存の行を削除した上で、行を追加、更新するようになっています。
参考: https://sqlite.org/lang_conflict.html
When a UNIQUE or PRIMARY KEY constraint violation occurs, the REPLACE algorithm deletes pre-existing rows that are causing the constraint violation prior to inserting or updating the current row
ですので REPLACE
の対象が ForeignKey
で指定されている Entity のレコードであった場合に意図しない動作が発生する場合があります。
例
記事
と お気に入りの記事
の Entity が以下のように定義されていたとします。
(お気に入りの記事
では ForeignKey
で 記事
の Entity が指定されています)
// 記事
@Entity(tableName = "articles")
data class Article(
@PrimaryKey val id: Int,
val title: String,
val contents: String
)
// お気に入りの記事
@Entity(
tableName = "favorite_articles",
foreignKeys = [
ForeignKey(
entity = Article::class,
parentColumns = ["id"],
childColumns = ["articleId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class FavoriteArticle(
@PrimaryKey val articleId: Int
)
そして Dao で以下のようなメソッドを定義していたとします。
@Dao
abstract class ArticleDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insertArticle(article: Article)
...
}
そして 記事
と お気に入りの記事
のレコードがそれぞれデータベースに保存されていた場合に、上記の Dao のメソッドで既存の 記事
を更新するとデータベースに保存されている関連した お気に入りの記事
が削除されてしまいます。
(ForeignKey
の onDelete
に設定している値によって お気に入りの記事
がどうなるか挙動は変わりますが、この例の場合だと ForeignKey.CASCADE
を設定しているので削除されます)
解決方法
例えば以下の例のように OnConflictStrategy.IGNORE
などに設定しておき、Transaction を張ってレコードがあれば更新、なければ新規作成のようにする方法が考えられます。
@Dao
abstract class ArticleDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insertArticle(article: Article)
@Query("UPDATE articles SET title = :title, contents = :contents WHERE id = :articleId")
abstract fun updateArticle(articleId: Int, title: String, contents: String)
@Query("SELECT COUNT(*) FROM articles WHERE id = :articleId")
abstract fun countArticle(articleId: Int): Int
@Transaction
open fun insertOrUpdateArticle(article: Article) {
if (countArticle(article.id) > 0) {
// レコードがあれば更新
updateArticle(article.id, article.title, article.contents)
} else {
// レコードがなければ新規作成
insertArticle(article)
}
}
...
}