次のような Entity を例に @Insert, @Update, @Upsert の動きを見てみます。
@Entity(
tableName = "sample_table",
indices = [Index(value = ["unique_id"], unique = true)]
)
data class SampleEntity(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String,
@ColumnInfo(name = "name")
val name: String,
@ColumnInfo(name = "age")
val age: Int,
@ColumnInfo(name = "unique_id")
val uniqueId: String
)
@Dao
interface SampleDao {
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(entity: SampleEntity)
@Update
suspend fun update(entity: SampleEntity)
@Upsert
suspend fun upsert(entity: SampleEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrReplace(entity: SampleEntity)
@Query("SELECT * FROM sample_table")
suspend fun getAll(): List<SampleEntity>
}
この記事では Room の in memory database builder を使い、簡単にユニットテストで動きを確かめています。
@RunWith(RobolectricTestRunner::class)
class ExampleUnitTest {
private lateinit var db: SampleDatabase
private lateinit var dao: SampleDao
@Before
fun prep() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(context, SampleDatabase::class.java).build()
dao = db.companionDeviceDao()
}
@After
fun tearDown() {
db.close()
}
}
@Insert(onConflict = OnConflictStrategy.REPLACE)
ここで説明する REPLACE の挙動はすべて SQLite のドキュメントと整合しています。
sqlite.org
PrimaryKey が重複したとき
REPLACE なので、PrimaryKey が既存レコードと重複した場合は既存レコードが更新されます。
内部的に REPLACE は既存レコードを一旦削除したうえで新たにレコードを作成することで更新とします。
@Test
fun `insertOrReplace - insert then replace`() = runTest {
val entity1 = SampleEntity("id1", "name", 12, "uid1")
val entity2 = SampleEntity("id1", "hoge", 14, "uid2")
dao.insertOrReplace(entity1)
try {
dao.insertOrReplace(entity2)
} catch (e: SQLiteConstraintException) {
fail("error is not expected: $e")
}
val result = dao.getAll()
assertTrue(result.containsAll(listOf(entity2)))
}
Unique Index が重複したとき1
既存レコードを replace する場面で Unique Index が重複すると、既存レコードの削除に加え重複する Unique Index を持つ別の既存レコードも削除します。
@Test
fun `insertOrReplace - insert then replace - unique id conflicts`() = runTest {
val entity1 = SampleEntity("id1", "name", 12, "uid1")
val entity2 = SampleEntity("id2", "hoge", 14, "uid2")
val entity3 = SampleEntity("id1", "hoge", 14, "uid2")
dao.insertOrReplace(entity1)
dao.insertOrReplace(entity2)
dao.insertOrReplace(entity3)
val result = dao.getAll()
assertTrue(result.containsAll(listOf(entity3)))
}
Unique Index が重複したとき2
PrimaryKey は重複せず Unique Index が重複するときも、重複する Unique Index を持つ既存レコードが削除されます。
@Test
fun `insertOrReplace - insert 2 times`() = runTest {
val entity1 = SampleEntity("id1", "name", 12, "uid1")
val entity2 = SampleEntity("id2", "hoge", 14, "uid1")
dao.insertOrReplace(entity1)
dao.insertOrReplace(entity2)
val result = dao.getAll()
assertTrue(result.containsAll(listOf(entity2)))
}
@Update
Unique Index が重複したとき
Unique Index の重複はゆるされないので update は失敗し、SQLiteConstraintException がスローされます。
@Insert(onConflict = OnConflictStrategy.REPLACE) とは異なり、元のデータは変更されず update を呼び出す直前のままです。
@Test
fun `insert and update - unique id conflicts`() = runTest {
val entity1 = SampleEntity("id1", "name", 12, "uid1")
val entity2 = SampleEntity("id2", "hoge", 14, "uid2")
val entity3 = SampleEntity("id1", "hoge", 14, "uid2")
dao.insert(entity1)
dao.insert(entity2)
try {
dao.update(entity3)
fail("can't complete update as unique id conflicts")
} catch (_: SQLiteConstraintException) {
}
val result = dao.getAll()
assertTrue(result.containsAll(listOf(entity1, entity2)))
}
@Upsert
@Upsert は PrimaryKey の重複するデータがあれば Update, なければ Insert とする処理です。
次に示すテストでは entity1 の upsert は Insert、entity2 の upsert は Update になります。
@Test
fun `upsert - insert then update`() = runTest {
val entity1 = SampleEntity("id1", "name", 12, "uid1")
val entity2 = SampleEntity("id1", "hoge", 14, "uid2")
dao.upsert(entity1)
try {
dao.upsert(entity2)
} catch (e: SQLiteConstraintException) {
fail("error is not expected: $e")
}
val result = dao.getAll()
assertTrue(result.containsAll(listOf(entity2)))
}
Unique Index が重複する Update 1
Unique Index が重複することになる Update をかける Upsert をする場合、Unique Index の制約に違反する Update を実行した時点で SQLiteConstraintExcpetion がスローされます。
@Insert(onConflict = OnConflictStrategy.REPLACE) とは異なり、元のデータは変更されず upsert を呼び出す直前のままです。
@Test
fun `upsert - insert then update - unique id conflicts`() = runTest {
val entity1 = SampleEntity("id1", "name", 12, "uid1")
val entity2 = SampleEntity("id2", "hoge", 14, "uid2")
val entity3 = SampleEntity("id1", "hoge", 14, "uid2")
dao.upsert(entity1)
dao.upsert(entity2)
try {
dao.upsert(entity3)
fail("can't complete upsert as unique id conflicts")
} catch (_: SQLiteConstraintException) {
}
val result = dao.getAll()
assertTrue(result.containsAll(listOf(entity1, entity2)))
}
Unique Index が重複する Update 2
PrimaryKey が異なり Unique Index が重複する Upsert をする場合、処理自体は Insert 相当になるはずですが Upsert の関数は例外をスローせず、かつ @Insert(onConflict = OnConflictStrategy.REPLACE) のような削除もしません。
結果的にデータの変更は何も起きないが例外もないことになります。この場合 upsert 関数に返り値を定義し、変更された行数を数えることで upsert がうまくいったかどうか判断する必要があります。
@Test
fun `upsert - insert 2 times - unique id conflicts`() = runTest {
val entity1 = SampleEntity("id1", "name", 12, "uid1")
val entity2 = SampleEntity("id2", "hoge", 14, "uid1")
dao.upsert(entity1)
dao.upsert(entity2)
val result = dao.getAll()
assertTrue(result.containsAll(listOf(entity1)))
}