【问题标题】:Android Room Persistence Library: UpsertAndroid 房间持久性库:Upsert
【发布时间】:2018-01-22 10:51:21
【问题描述】:

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

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

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

【问题讨论】:

    标签: android sqlite android-room android-architecture-components


    【解决方案1】:

    我找不到一个可以插入或更新而不会对我的外键造成不必要更改的 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);
    }
    

    【讨论】:

    • 您可能希望提高效率并检查返回值。 -1 表示任何类型的冲突。
    • 最好用@Transaction注解标记upsert方法
    • 我想正确的方法是询问该值是否已经在数据库中(使用其主键)。您可以使用 abstractClass(替换 dao 接口)或使用调用对象 dao 的类来做到这一点
    • @Ohmnibus no ,因为文档说 > 将此注释放在插入、更新或删除方法上没有影响,因为它们总是在事务中运行。类似地,如果它使用 Query 注释但运行更新或删除语句,它会自动包装在事务中。 See Transaction doc
    • @LevonVardanyan 您链接的页面中的示例显示了一种与 upsert 非常相似的方法,其中包含插入和删除。另外,我们不是将注解放到插入或更新中,而是放到包含两者的方法中。
    【解决方案2】:

    对于更优雅的方式,我建议两个选项:

    检查insert 操作的返回值,将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);   
        }
    }
    

    处理来自insert 操作的异常,使用FAIL 作为OnConflictStrategy:

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

    【讨论】:

    • 这适用于单个实体,但很难用于集合。最好过滤插入了哪些集合并将它们从更新中过滤掉。
    • @DanielWilson 这取决于您的应用程序,此答案适用于单个实体,但不适用于我拥有的实体列表。
    • 无论出于何种原因,当我执行第一种方法时,插入一个已经存在的 ID 会返回一个大于现有 ID 的行号,而不是 -1L。
    • 正如 Ohmnibus 在另一个答案中所说,最好用 upsert 注释标记 upsert 方法 - stackoverflow.com/questions/45677230/…
    • 你能解释一下为什么@Update注解有一个FAIL或IGNORE的冲突策略吗?在什么情况下,Room 会认为更新查询有冲突?如果我天真地解释 Update 注释上的冲突策略,我会说当有要更新的东西时存在冲突,因此它永远不会更新。但这不是我看到的行为。更新查询甚至会发生冲突吗?或者,如果更新导致另一个唯一键约束失败,是否会发生冲突?
    【解决方案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") 进行更复杂的事务

    【讨论】:

      【解决方案4】:

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

      例如:

      private void upsert(EntityA entityA) {
         EntityA existingEntityA = getEntityA("query1","query2");
         if (existingEntityA == null) {
            insert(entityA);
         } else {
            entityA.setParam(existingEntityA.getParam());
            update(entityA);
         }
      }
      

      【讨论】:

        【解决方案5】:

        也许你可以把你的 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);
                }
            }
        }
        

        【讨论】:

        • 但是,没有“在 for 循环中插入”。
        • 你说得对!我错过了,我以为你正在插入 for 循环。这是一个很好的解决方案。
        • 这是黄金。这让我看到了 Florina 的帖子,你应该阅读:medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 — 感谢@yeonseok.seo 的提示!
        • 当你检查id是否为-1时,不应该是-1L吗?还是根本不重要?
        • @PRA 据我所知,这根本不重要。 docs.oracle.com/javase/specs/jls/se8/html/… Long 将被拆箱为 long 并执行整数相等测试。如果我错了,请指出正确的方向。
        【解决方案6】:

        这种说法应该是可能的:

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

        【讨论】:

        • 什么意思? Room @Queryannotation 不支持 ON CONFLICT UPDATE SET a = 1, b = 2。
        【解决方案7】:

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

        @Insert(onConflict = OnConflictStrategy.REPLACE)
        

        替换一行。

        参考 - Go to tips Android Room Codelab

        【讨论】:

        • 请不要使用这种方法。如果您有任何外键查看您的数据,它将触发 onDelete 侦听器,您可能不希望这样
        • @AlexandrZhurkov,我想它应该只在更新时触发,然后任何侦听器如果实现它都会正确执行。无论如何,如果我们有数据监听器和 onDelete 触发器,那么它必须由代码处理
        • @AlexandrZhurkov 这在使用外键的实体上设置deferred = true 时效果很好。
        • @ubuntudroid 即使在实体外键上设置该标志也不能正常工作,刚刚测试过。一旦事务完成,删除调用仍然会进行,因为它不会在此过程中被解除,它只是在它发生时不会发生,但在事务结束时仍然存在。
        【解决方案8】:

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

        【讨论】:

        • long id = insert(entity) 应该是 val id = insert(entity) for kotlin
        • @Sam,如何处理 null values 我不想用 null 更新但保留旧值的地方。 ?
        【解决方案9】:

        如果您有遗留代码:Java 和 BaseDao as Interface 中的某些实体(您无法在其中添加函数体),或者您懒得将所有 implements 替换为 Java 子代的 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)
            }
        }
        

        【讨论】:

        • 这似乎是有缺陷的?它没有正确创建交易。
        【解决方案10】:

        我发现了一篇有趣的文章here。

        它与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)
        

        【讨论】:

        • 但是您执行了多个update 请求...如果您有一个包含 100 个项目的列表怎么办?
        【解决方案11】:

        或者像@yeonseok.seo 帖子中建议的那样在循环中手动进行UPSERT,我们可以在Android Room 中使用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)
        

        【讨论】:

        • real upsert 的唯一答案...我感觉其他发帖者就是不明白upsert 的主要特点是能够更新当你不知道它的 ID 时出现一行。使用upsert DB 可以只使用唯一约束自动更新行,无需主键,无需额外请求。
        • 是的,这是来自 Sqlite 的真正 UPSERT。但是您可以看到它仅在 Android 11 和 12 中受支持,但在之前的版本中不受支持。现在,Android Room 仍然不支持 Android 11 和 12 中的 UPSERT 功能注释,即使具有此版本的设备上的 Sqlite 支持它。因此,我们只有@Query("") 选项可以在 Android 11 和 12 上调用真正的 UPSERT 功能。此外,这里的大部分答案都是在没有 Android 11 和 12 的时候发布的,因此设备上的 Sqlite 版本不支持 UPSERT ,这就是为什么人们不得不使用一些变通方法。
        【解决方案12】:

        这是在Room 库中使用real UPSERT 子句的方法。

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

        1. 在您的项目中设置 Android SQLite support library 以在所有设备上使用现代 SQLite 功能:
        2. 从 BasicDao 继承您的 daos。
        3. 您可能想在 BasicEntity 中添加:abstract fun toMap(): Map&lt;String, Any?&gt;

        在你的道中使用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
                )
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-05-27
          • 1970-01-01
          • 2020-03-19
          • 2017-10-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多