Android Room持久化库:Upsert

138

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

Sqlite没有本地支持upsert的功能,解决方法在这个SO问题中有描述。鉴于那里的解决方案,如何将它们应用到Room中?

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

14个回答

0

这里介绍一种在Room库中使用真正的UPSERT子句的方法。

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

  1. 在项目中设置Android SQLite支持库,以在所有设备上使用现代SQLite功能:
  2. 从BasicDao继承您的daos。
  3. 可能,您想在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"))

BasicDao:

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
        )
    }
}

0
另一种想到的方法是通过查询DAO获取实体,然后执行任何所需的更新。
与本线程中其他解决方案相比,由于需要检索完整实体,因此在运行时可能不那么高效,但允许更多操作的灵活性,例如可以更新哪些字段/变量。
例如:
private void upsert(EntityA entityA) {
   EntityA existingEntityA = getEntityA("query1","query2");
   if (existingEntityA == null) {
      insert(entityA);
   } else {
      entityA.setParam(existingEntityA.getParam());
      update(entityA);
   }
}

-1

使用这种语句应该是可行的:

INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2

什么意思?ON CONFLICT UPDATE SET a = 1, b = 2Room@Query注释中不受支持。 - isabsent

-2

如果您有遗留代码:一些Java实体和BaseDao作为接口(您无法添加函数体),或者您太懒了,不想用extends替换所有Java子类中的implements

注意:它仅适用于Kotlin代码。我相信您会用Kotlin编写新代码,我是对的吧? :)

最后一个懒惰的解决方案是添加两个Kotlin扩展函数

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)
    }
}

这似乎有缺陷?它不能正确地创建一个事务。 - Rawa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接