124

Android 的 Room 持久性库优雅地包含了适用于对象或集合的 @Insert 和 @Update 注释。但是,我有一个需要 UPSERT 的用例(包含模型的推送通知),因为数据库中可能存在也可能不存在数据。

Sqlite 本身没有 upsert,在这个SO question中描述了解决方法。鉴于那里的解决方案,如何将它们应用于 Room?

更具体地说,如何在 Room 中实现不会破坏任何外键约束的插入或更新?使用带有 onConflict=REPLACE 的 insert 将导致调用该行的任何外键的 onDelete。在我的情况下,onDelete 会导致级联,重新插入一行将导致其他表中具有外键的行被删除。这不是预期的行为。

4

12 回答 12

111

也许你可以让你的 BaseDao 像这样。

使用@Transaction 保护 upsert 操作,并仅在插入失败时尝试更新。

@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);
        }
    }
}
于 2018-06-07T08:34:26.080 回答
90

为了更优雅的方式来做到这一点,我建议两个选项:

insert使用IGNOREas a检查操作的返回值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);   
    }
}

使用as处理来自insert操作的异常:FAILOnConflictStrategy

@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);
    }
}
于 2018-02-06T11:17:27.540 回答
41

我找不到一个可以插入或更新而不会对我的外键造成不必要更改的 SQLite 查询,因此我选择先插入,如果发生冲突则忽略它们,然后立即更新,再次忽略冲突。

insert 和 update 方法受到保护,因此外部类只能看到和使用 upsert 方法。请记住,这不是真正的 upsert,就好像任何 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);
}
于 2017-08-15T00:42:02.983 回答
10

如果表格有多于一列,您可以使用

@Insert(onConflict = OnConflictStrategy.REPLACE)

替换一行。

参考 -转到提示 Android Room Codelab

于 2018-07-13T08:09:51.697 回答
6

这是 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)
  }
}
于 2019-08-21T15:56:03.540 回答
3

只是关于如何使用 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") 进行更复杂的事务

于 2018-03-07T23:59:47.340 回答
2

或者,要像@yeonseok.seo 帖子中建议的那样在循环中手动进行 UPSERT,我们可以使用UPSERTAndroid Room 中的 Sqlite v.3.24.0 提供的功能。

如今,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)
于 2021-11-12T12:45:04.823 回答
2

我在这里找到了一篇关于它的有趣读物。

它与https://stackoverflow.com/a/50736568/4744263上发布的“相同” 。但是,如果你想要一个惯用且干净的 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)
于 2021-03-23T01:41:24.087 回答
0

我能想到的另一种方法是通过 DAO 通过查询获取实体,然后执行任何所需的更新。由于必须检索完整实体,因此与该线程中的其他解决方案相比,这可能在运行时方面效率较低,但在允许的操作方面允许更大的灵活性,例如更新哪些字段/变量。

例如 :

private void upsert(EntityA entityA) {
   EntityA existingEntityA = getEntityA("query1","query2");
   if (existingEntityA == null) {
      insert(entityA);
   } else {
      entityA.setParam(existingEntityA.getParam());
      update(entityA);
   }
}
于 2018-05-03T17:10:39.613 回答
0

这是在库中使用真实 UPSERT子句的一种方法。Room

此方法的主要优点是您可以更新不知道其 ID 的行

  1. 在您的项目中设置Android SQLite 支持库以在所有设备上使用现代 SQLite 功能:
  2. 从 BasicDao 继承你的 daos。
  3. 可能,您想在 BasicEntity 中添加:abstract fun toMap(): Map<String, Any?>

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
        )
    }
}
于 2022-01-01T19:35:59.460 回答
-1

这种陈述应该是可能的:

INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2
于 2018-07-12T13:10:44.740 回答
-2

如果您有遗留代码:Java 中的某些实体和BaseDao as Interface(您无法添加函数体)或者您懒得用 Java-children 替换所有implements实体extends

注意:它仅适用于 Kotlin 代码。我确定你用 Kotlin 编写新代码,我是对的?:)

最后一个懒惰的解决方案是添加两个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)
    }
}
于 2020-09-29T07:01:34.703 回答