次のような 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() } // ... unit test follows }
@Insert(onConflict = OnConflictStrategy.REPLACE)
ここで説明する REPLACE の挙動はすべて SQLite のドキュメントと整合しています。
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") // insert dao.insertOrReplace(entity1) try { // replace with entity2, can't fail dao.insertOrReplace(entity2) } catch (e: SQLiteConstraintException) { fail("error is not expected: $e") } val result = dao.getAll() // only entity2 exists 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") // insert as first entity dao.insertOrReplace(entity1) // insert as pk/unique id is different dao.insertOrReplace(entity2) // replace entity1 with unique id conflicting with entity 2 // then remove both entity1 and entity2 then insert entity3 dao.insertOrReplace(entity3) val result = dao.getAll() // only entity3 exists 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") // insert dao.insertOrReplace(entity1) // insert as pk is different from entity1, but conflicting unique id dao.insertOrReplace(entity2) val result = dao.getAll() // only entity2 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") // insert dao.insert(entity1) // insert dao.insert(entity2) try { // update entity 1 to make unique id conflict with entity2 dao.update(entity3) fail("can't complete update as unique id conflicts") } catch (_: SQLiteConstraintException) { // expected } 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") // insert dao.upsert(entity1) try { // update entity2, can't fail 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") // insert as first entity dao.upsert(entity1) // insert as pk is different dao.upsert(entity2) try { // update entity1 with unique id conflicts with entity 2 dao.upsert(entity3) fail("can't complete upsert as unique id conflicts") } catch (_: SQLiteConstraintException) { // expected } 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") // insert dao.upsert(entity1) // try insert as pk is different from entity1, but conflicting unique id with entity1 // this function won't throw exception for SQL Constraint violation, but just // fails to insert entity2 dao.upsert(entity2) val result = dao.getAll() // only entity1 exists assertTrue(result.containsAll(listOf(entity1))) }