Android 房间持久性库:Upsert
Android Room Persistence Library: Upsert
Android 的 Room 持久性库优雅地包含适用于对象或集合的 @Insert 和 @Update 注释。但是,我有一个用例(包含模型的推送通知)需要 UPSERT,因为数据库中可能存在也可能不存在数据。
Sqlite 本身没有更新插入,SO question 中描述了变通方法。鉴于那里的解决方案,如何将它们应用于 Room?
更具体地说,我如何在 Room 中实现不破坏任何外键约束的插入或更新?使用带有 onConflict=REPLACE 的插入将导致调用该行的任何外键的 onDelete。在我的例子中,onDelete 导致级联,重新插入一行将导致其他表中具有外键的行被删除。这不是预期的行为。
我找不到一个 SQLite 查询可以插入或更新而不会对我的外键造成不必要的更改,所以我选择先插入,如果发生冲突则忽略,然后立即更新,再次忽略冲突。
插入和更新方法受到保护,因此外部 类 只能看到和使用更新插入方法。请记住,这不是真正的更新插入,因为如果任何 MyEntity POJOS 具有空字段,它们将覆盖数据库中当前可能存在的内容。这对我来说不是警告,但可能适用于您的应用程序。
@Insert(onConflict = OnConflictStrategy.IGNORE)
protected abstract void insert(List<MyEntity> entities);
@Update(onConflict = OnConflictStrategy.IGNORE)
protected abstract void update(List<MyEntity> entities);
@Transaction
public void upsert(List<MyEntity> entities) {
insert(models);
update(models);
}
为了更优雅地做到这一点,我建议两个选项:
检查来自 insert
操作的 return 值,使用 IGNORE
作为 OnConflictStrategy
(如果它等于 -1,则表示未插入行):
@Insert(onConflict = OnConflictStrategy.IGNORE)
long insert(Entity entity);
@Update(onConflict = OnConflictStrategy.IGNORE)
void update(Entity entity);
@Transaction
public void upsert(Entity entity) {
long id = insert(entity);
if (id == -1) {
update(entity);
}
}
使用 FAIL
作为 OnConflictStrategy
处理来自 insert
操作的异常:
@Insert(onConflict = OnConflictStrategy.FAIL)
void insert(Entity entity);
@Update(onConflict = OnConflictStrategy.FAIL)
void update(Entity entity);
@Transaction
public void upsert(Entity entity) {
try {
insert(entity);
} catch (SQLiteConstraintException exception) {
update(entity);
}
}
只是关于如何使用 Kotlin 保留模型数据执行此操作的更新(可能像示例中那样在计数器中使用它):
//Your Dao must be an abstract class instead of an interface (optional database constructor variable)
@Dao
abstract class ModelDao(val database: AppDatabase) {
@Insert(onConflict = OnConflictStrategy.FAIL)
abstract fun insertModel(model: Model)
//Do a custom update retaining previous data of the model
//(I use constants for tables and column names)
@Query("UPDATE $MODEL_TABLE SET $COUNT=$COUNT+1 WHERE $ID = :modelId")
abstract fun updateModel(modelId: Long)
//Declare your upsert function open
open fun upsert(model: Model) {
try {
insertModel(model)
}catch (exception: SQLiteConstraintException) {
updateModel(model.id)
}
}
}
您还可以使用@Transaction 和数据库构造函数变量来使用 database.openHelper.writableDatabase.execSQL("SQL STATEMENT")
进行更复杂的交易
我能想到的另一种方法是通过 DAO 通过查询获取实体,然后执行任何需要的更新。
由于必须检索完整的实体,因此在运行时方面与该线程中的其他解决方案相比,这可能效率较低,但在允许的操作方面允许更多的灵活性,例如 fields/variable 要更新的内容。
例如:
private void upsert(EntityA entityA) {
EntityA existingEntityA = getEntityA("query1","query2");
if (existingEntityA == null) {
insert(entityA);
} else {
entityA.setParam(existingEntityA.getParam());
update(entityA);
}
}
也许你可以像这样制作你的BaseDao。
使用@Transaction 保护更新插入操作,
并仅在插入失败时才尝试更新。
@Dao
public abstract class BaseDao<T> {
/**
* Insert an object in the database.
*
* @param obj the object to be inserted.
* @return The SQLite row id
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
public abstract long insert(T obj);
/**
* Insert an array of objects in the database.
*
* @param obj the objects to be inserted.
* @return The SQLite row ids
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
public abstract List<Long> insert(List<T> obj);
/**
* Update an object from the database.
*
* @param obj the object to be updated
*/
@Update
public abstract void update(T obj);
/**
* Update an array of objects from the database.
*
* @param obj the object to be updated
*/
@Update
public abstract void update(List<T> obj);
/**
* Delete an object from the database
*
* @param obj the object to be deleted
*/
@Delete
public abstract void delete(T obj);
@Transaction
public void upsert(T obj) {
long id = insert(obj);
if (id == -1) {
update(obj);
}
}
@Transaction
public void upsert(List<T> objList) {
List<Long> insertResult = insert(objList);
List<T> updateList = new ArrayList<>();
for (int i = 0; i < insertResult.size(); i++) {
if (insertResult.get(i) == -1) {
updateList.add(objList.get(i));
}
}
if (!updateList.isEmpty()) {
update(updateList);
}
}
}
应该可以用这种语句:
INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2
如果table有不止一列,您可以使用
@Insert(onConflict = OnConflictStrategy.REPLACE)
替换一行。
这是 Kotlin 中的代码:
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(entity: Entity): Long
@Update(onConflict = OnConflictStrategy.REPLACE)
fun update(entity: Entity)
@Transaction
fun upsert(entity: Entity) {
val id = insert(entity)
if (id == -1L) {
update(entity)
}
}
如果您有遗留代码:Java 和 BaseDao as Interface
中的某些实体(您无法在其中添加函数体)或者您懒得将所有 implements
替换为 [=13] =] 对于 Java-children.
Note: It works only in Kotlin code. I'm sure that you write new code in Kotlin, I'm right? :)
最后一个偷懒的办法就是加两个Kotlin Extension functions
:
fun <T> BaseDao<T>.upsert(entityItem: T) {
if (insert(entityItem) == -1L) {
update(entityItem)
}
}
fun <T> BaseDao<T>.upsert(entityItems: List<T>) {
val insertResults = insert(entityItems)
val itemsToUpdate = arrayListOf<T>()
insertResults.forEachIndexed { index, result ->
if (result == -1L) {
itemsToUpdate.add(entityItems[index])
}
}
if (itemsToUpdate.isNotEmpty()) {
update(itemsToUpdate)
}
}
我发现了一篇关于它的有趣读物 here。
它与 上发布的“相同”。但是,如果你想要一个惯用的和干净的 Kotlin 版本,你可以这样做:
@Transaction
open fun insertOrUpdate(objList: List<T>) = insert(objList)
.withIndex()
.filter { it.value == -1L }
.forEach { update(objList[it.index]) }
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(obj: List<T>): List<Long>
@Update
abstract fun update(obj: T)
或者像 @yeonseok.seo post 中建议的那样在循环中手动制作 UPSERT,我们可以使用 [=20= 中 Sqlite v.3.24.0 提供的 UPSERT
功能]房间。
如今,Android 11 和 12 分别支持默认的 Sqlite 版本 3.28.0 和 3.32.2。如果您在 Android 11 之前的版本中需要它,您可以将默认的 Sqlite 替换为像这样的自定义 Sqlite 项目 https://github.com/requery/sqlite-android (或构建您自己的项目)以获得最新 Sqlite 版本中可用的此功能和其他功能,但是在默认提供的 Android Sqlite 中不可用。
如果您的设备上的 Sqlite 版本从 3.24.0 开始,您可以在 Android Room 中使用 UPSERT,如下所示:
@Query("INSERT INTO Person (name, phone) VALUES (:name, :phone) ON CONFLICT (name) DO UPDATE SET phone=excluded.phone")
fun upsert(name: String, phone: String)
这是在 Room
库中使用 real UPSERT
子句的方法。
此方法的主要优点是您可以更新不知道其 ID 的行。
- 在您的项目中设置 Android SQLite support library 以在所有设备上使用现代 SQLite 功能:
- 从 BasicDao 继承你的 daos。
- 您可能想在 BasicEntity 中添加:
abstract fun toMap(): Map<String, Any?>
在你的 Dao 中使用 UPSERT
:
@Transaction
private suspend fun upsert(entity: SomeEntity): Map<String, Any?> {
return upsert(
SomeEntity.TABLE_NAME,
entity.toMap(),
setOf(SomeEntity.SOME_UNIQUE_KEY),
setOf(SomeEntity.ID),
)
}
// An entity has been created. You will get ID.
val rawEntity = someDao.upsert(SomeEntity(0, "name", "key-1"))
// An entity has been updated. You will get ID too, despite you didn't know it before, just by unique constraint!
val rawEntity = someDao.upsert(SomeEntity(0, "new name", "key-1"))
基本道:
import android.database.Cursor
import androidx.room.*
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
abstract class BasicDao(open val database: RoomDatabase) {
/**
* Upsert all fields of the entity except those specified in [onConflict] and [excludedColumns].
*
* Usually, you don't want to update PK, you can exclude it in [excludedColumns].
*
* [UPSERT](https://www.sqlite.org/lang_UPSERT.html) syntax supported since version 3.24.0 (2018-06-04).
* [RETURNING](https://www.sqlite.org/lang_returning.html) syntax supported since version 3.35.0 (2021-03-12).
*/
protected suspend fun upsert(
table: String,
entity: Map<String, Any?>,
onConflict: Set<String>,
excludedColumns: Set<String> = setOf(),
returning: Set<String> = setOf("*")
): Map<String, Any?> {
val updatableColumns = entity.keys
.filter { it !in onConflict && it !in excludedColumns }
.map { "`${it}`=excluded.`${it}`" }
// build sql
val comma = ", "
val placeholders = entity.map { "?" }.joinToString(comma)
val returnings = returning.joinToString(comma) { if (it == "*") it else "`${it}`" }
val sql = "INSERT INTO `${table}` VALUES (${placeholders})" +
" ON CONFLICT(${onConflict.joinToString(comma)}) DO UPDATE SET" +
" ${updatableColumns.joinToString(comma)}" +
" RETURNING $returnings"
val query: SupportSQLiteQuery = SimpleSQLiteQuery(sql, entity.values.toTypedArray())
val cursor: Cursor = database.openHelper.writableDatabase.query(query)
return getCursorResult(cursor).first()
}
protected fun getCursorResult(cursor: Cursor, isClose: Boolean = true): List<Map<String, Any?>> {
val result = mutableListOf<Map<String, Any?>>()
while (cursor.moveToNext()) {
result.add(cursor.columnNames.mapIndexed { index, columnName ->
val columnValue = if (cursor.isNull(index)) null else cursor.getString(index)
columnName to columnValue
}.toMap())
}
if (isClose) {
cursor.close()
}
return result
}
}
实体示例:
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = SomeEntity.TABLE_NAME,
indices = [Index(value = [SomeEntity.SOME_UNIQUE_KEY], unique = true)]
)
data class SomeEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = ID)
val id: Long,
@ColumnInfo(name = NAME)
val name: String,
@ColumnInfo(name = SOME_UNIQUE_KEY)
val someUniqueKey: String,
) {
companion object {
const val TABLE_NAME = "some_table"
const val ID = "id"
const val NAME = "name"
const val SOME_UNIQUE_KEY = "some_unique_key"
}
fun toMap(): Map<String, Any?> {
return mapOf(
ID to if (id == 0L) null else id,
NAME to name,
SOME_UNIQUE_KEY to someUniqueKey
)
}
}
Android 的 Room 持久性库优雅地包含适用于对象或集合的 @Insert 和 @Update 注释。但是,我有一个用例(包含模型的推送通知)需要 UPSERT,因为数据库中可能存在也可能不存在数据。
Sqlite 本身没有更新插入,SO question 中描述了变通方法。鉴于那里的解决方案,如何将它们应用于 Room?
更具体地说,我如何在 Room 中实现不破坏任何外键约束的插入或更新?使用带有 onConflict=REPLACE 的插入将导致调用该行的任何外键的 onDelete。在我的例子中,onDelete 导致级联,重新插入一行将导致其他表中具有外键的行被删除。这不是预期的行为。
我找不到一个 SQLite 查询可以插入或更新而不会对我的外键造成不必要的更改,所以我选择先插入,如果发生冲突则忽略,然后立即更新,再次忽略冲突。
插入和更新方法受到保护,因此外部 类 只能看到和使用更新插入方法。请记住,这不是真正的更新插入,因为如果任何 MyEntity POJOS 具有空字段,它们将覆盖数据库中当前可能存在的内容。这对我来说不是警告,但可能适用于您的应用程序。
@Insert(onConflict = OnConflictStrategy.IGNORE)
protected abstract void insert(List<MyEntity> entities);
@Update(onConflict = OnConflictStrategy.IGNORE)
protected abstract void update(List<MyEntity> entities);
@Transaction
public void upsert(List<MyEntity> entities) {
insert(models);
update(models);
}
为了更优雅地做到这一点,我建议两个选项:
检查来自 insert
操作的 return 值,使用 IGNORE
作为 OnConflictStrategy
(如果它等于 -1,则表示未插入行):
@Insert(onConflict = OnConflictStrategy.IGNORE)
long insert(Entity entity);
@Update(onConflict = OnConflictStrategy.IGNORE)
void update(Entity entity);
@Transaction
public void upsert(Entity entity) {
long id = insert(entity);
if (id == -1) {
update(entity);
}
}
使用 FAIL
作为 OnConflictStrategy
处理来自 insert
操作的异常:
@Insert(onConflict = OnConflictStrategy.FAIL)
void insert(Entity entity);
@Update(onConflict = OnConflictStrategy.FAIL)
void update(Entity entity);
@Transaction
public void upsert(Entity entity) {
try {
insert(entity);
} catch (SQLiteConstraintException exception) {
update(entity);
}
}
只是关于如何使用 Kotlin 保留模型数据执行此操作的更新(可能像示例中那样在计数器中使用它):
//Your Dao must be an abstract class instead of an interface (optional database constructor variable)
@Dao
abstract class ModelDao(val database: AppDatabase) {
@Insert(onConflict = OnConflictStrategy.FAIL)
abstract fun insertModel(model: Model)
//Do a custom update retaining previous data of the model
//(I use constants for tables and column names)
@Query("UPDATE $MODEL_TABLE SET $COUNT=$COUNT+1 WHERE $ID = :modelId")
abstract fun updateModel(modelId: Long)
//Declare your upsert function open
open fun upsert(model: Model) {
try {
insertModel(model)
}catch (exception: SQLiteConstraintException) {
updateModel(model.id)
}
}
}
您还可以使用@Transaction 和数据库构造函数变量来使用 database.openHelper.writableDatabase.execSQL("SQL STATEMENT")
进行更复杂的交易我能想到的另一种方法是通过 DAO 通过查询获取实体,然后执行任何需要的更新。 由于必须检索完整的实体,因此在运行时方面与该线程中的其他解决方案相比,这可能效率较低,但在允许的操作方面允许更多的灵活性,例如 fields/variable 要更新的内容。
例如:
private void upsert(EntityA entityA) {
EntityA existingEntityA = getEntityA("query1","query2");
if (existingEntityA == null) {
insert(entityA);
} else {
entityA.setParam(existingEntityA.getParam());
update(entityA);
}
}
也许你可以像这样制作你的BaseDao。
使用@Transaction 保护更新插入操作, 并仅在插入失败时才尝试更新。
@Dao
public abstract class BaseDao<T> {
/**
* Insert an object in the database.
*
* @param obj the object to be inserted.
* @return The SQLite row id
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
public abstract long insert(T obj);
/**
* Insert an array of objects in the database.
*
* @param obj the objects to be inserted.
* @return The SQLite row ids
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
public abstract List<Long> insert(List<T> obj);
/**
* Update an object from the database.
*
* @param obj the object to be updated
*/
@Update
public abstract void update(T obj);
/**
* Update an array of objects from the database.
*
* @param obj the object to be updated
*/
@Update
public abstract void update(List<T> obj);
/**
* Delete an object from the database
*
* @param obj the object to be deleted
*/
@Delete
public abstract void delete(T obj);
@Transaction
public void upsert(T obj) {
long id = insert(obj);
if (id == -1) {
update(obj);
}
}
@Transaction
public void upsert(List<T> objList) {
List<Long> insertResult = insert(objList);
List<T> updateList = new ArrayList<>();
for (int i = 0; i < insertResult.size(); i++) {
if (insertResult.get(i) == -1) {
updateList.add(objList.get(i));
}
}
if (!updateList.isEmpty()) {
update(updateList);
}
}
}
应该可以用这种语句:
INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2
如果table有不止一列,您可以使用
@Insert(onConflict = OnConflictStrategy.REPLACE)
替换一行。
这是 Kotlin 中的代码:
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(entity: Entity): Long
@Update(onConflict = OnConflictStrategy.REPLACE)
fun update(entity: Entity)
@Transaction
fun upsert(entity: Entity) {
val id = insert(entity)
if (id == -1L) {
update(entity)
}
}
如果您有遗留代码:Java 和 BaseDao as Interface
中的某些实体(您无法在其中添加函数体)或者您懒得将所有 implements
替换为 [=13] =] 对于 Java-children.
Note: It works only in Kotlin code. I'm sure that you write new code in Kotlin, I'm right? :)
最后一个偷懒的办法就是加两个Kotlin Extension functions
:
fun <T> BaseDao<T>.upsert(entityItem: T) {
if (insert(entityItem) == -1L) {
update(entityItem)
}
}
fun <T> BaseDao<T>.upsert(entityItems: List<T>) {
val insertResults = insert(entityItems)
val itemsToUpdate = arrayListOf<T>()
insertResults.forEachIndexed { index, result ->
if (result == -1L) {
itemsToUpdate.add(entityItems[index])
}
}
if (itemsToUpdate.isNotEmpty()) {
update(itemsToUpdate)
}
}
我发现了一篇关于它的有趣读物 here。
它与
@Transaction
open fun insertOrUpdate(objList: List<T>) = insert(objList)
.withIndex()
.filter { it.value == -1L }
.forEach { update(objList[it.index]) }
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(obj: List<T>): List<Long>
@Update
abstract fun update(obj: T)
或者像 @yeonseok.seo post 中建议的那样在循环中手动制作 UPSERT,我们可以使用 [=20= 中 Sqlite v.3.24.0 提供的 UPSERT
功能]房间。
如今,Android 11 和 12 分别支持默认的 Sqlite 版本 3.28.0 和 3.32.2。如果您在 Android 11 之前的版本中需要它,您可以将默认的 Sqlite 替换为像这样的自定义 Sqlite 项目 https://github.com/requery/sqlite-android (或构建您自己的项目)以获得最新 Sqlite 版本中可用的此功能和其他功能,但是在默认提供的 Android Sqlite 中不可用。
如果您的设备上的 Sqlite 版本从 3.24.0 开始,您可以在 Android Room 中使用 UPSERT,如下所示:
@Query("INSERT INTO Person (name, phone) VALUES (:name, :phone) ON CONFLICT (name) DO UPDATE SET phone=excluded.phone")
fun upsert(name: String, phone: String)
这是在 Room
库中使用 real UPSERT
子句的方法。
此方法的主要优点是您可以更新不知道其 ID 的行。
- 在您的项目中设置 Android SQLite support library 以在所有设备上使用现代 SQLite 功能:
- 从 BasicDao 继承你的 daos。
- 您可能想在 BasicEntity 中添加:
abstract fun toMap(): Map<String, Any?>
在你的 Dao 中使用 UPSERT
:
@Transaction
private suspend fun upsert(entity: SomeEntity): Map<String, Any?> {
return upsert(
SomeEntity.TABLE_NAME,
entity.toMap(),
setOf(SomeEntity.SOME_UNIQUE_KEY),
setOf(SomeEntity.ID),
)
}
// An entity has been created. You will get ID.
val rawEntity = someDao.upsert(SomeEntity(0, "name", "key-1"))
// An entity has been updated. You will get ID too, despite you didn't know it before, just by unique constraint!
val rawEntity = someDao.upsert(SomeEntity(0, "new name", "key-1"))
基本道:
import android.database.Cursor
import androidx.room.*
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
abstract class BasicDao(open val database: RoomDatabase) {
/**
* Upsert all fields of the entity except those specified in [onConflict] and [excludedColumns].
*
* Usually, you don't want to update PK, you can exclude it in [excludedColumns].
*
* [UPSERT](https://www.sqlite.org/lang_UPSERT.html) syntax supported since version 3.24.0 (2018-06-04).
* [RETURNING](https://www.sqlite.org/lang_returning.html) syntax supported since version 3.35.0 (2021-03-12).
*/
protected suspend fun upsert(
table: String,
entity: Map<String, Any?>,
onConflict: Set<String>,
excludedColumns: Set<String> = setOf(),
returning: Set<String> = setOf("*")
): Map<String, Any?> {
val updatableColumns = entity.keys
.filter { it !in onConflict && it !in excludedColumns }
.map { "`${it}`=excluded.`${it}`" }
// build sql
val comma = ", "
val placeholders = entity.map { "?" }.joinToString(comma)
val returnings = returning.joinToString(comma) { if (it == "*") it else "`${it}`" }
val sql = "INSERT INTO `${table}` VALUES (${placeholders})" +
" ON CONFLICT(${onConflict.joinToString(comma)}) DO UPDATE SET" +
" ${updatableColumns.joinToString(comma)}" +
" RETURNING $returnings"
val query: SupportSQLiteQuery = SimpleSQLiteQuery(sql, entity.values.toTypedArray())
val cursor: Cursor = database.openHelper.writableDatabase.query(query)
return getCursorResult(cursor).first()
}
protected fun getCursorResult(cursor: Cursor, isClose: Boolean = true): List<Map<String, Any?>> {
val result = mutableListOf<Map<String, Any?>>()
while (cursor.moveToNext()) {
result.add(cursor.columnNames.mapIndexed { index, columnName ->
val columnValue = if (cursor.isNull(index)) null else cursor.getString(index)
columnName to columnValue
}.toMap())
}
if (isClose) {
cursor.close()
}
return result
}
}
实体示例:
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = SomeEntity.TABLE_NAME,
indices = [Index(value = [SomeEntity.SOME_UNIQUE_KEY], unique = true)]
)
data class SomeEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = ID)
val id: Long,
@ColumnInfo(name = NAME)
val name: String,
@ColumnInfo(name = SOME_UNIQUE_KEY)
val someUniqueKey: String,
) {
companion object {
const val TABLE_NAME = "some_table"
const val ID = "id"
const val NAME = "name"
const val SOME_UNIQUE_KEY = "some_unique_key"
}
fun toMap(): Map<String, Any?> {
return mapOf(
ID to if (id == 0L) null else id,
NAME to name,
SOME_UNIQUE_KEY to someUniqueKey
)
}
}