Android Room Persistence Library:Upsert


97

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

Sqlite本身没有upsert,因此在SO问题中描述了解决方法。给定那里的解决方案,如何将它们应用于Room?

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

Answers:


76

也许您可以像这样使您的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);
        }
    }
}

这将对性能不利,因为列表中的每个元素都会有多个数据库交互。
Tunji_D

13
但是,没有“在for循环中插入”。
yeonseok.seo

4
你是绝对正确的!我错过了,我以为您正在插入for循环。那是一个很好的解决方案。
Tunji_D

2
这是黄金。这使我想到了Florina的帖子,您应该阅读该帖子:medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1-感谢@ yeonseok.seo的提示!
Benoit Duffez '18

1
据我所知,@ PRA没关系。docs.oracle.com/javase/specs/jls/se8/html/…Long将取消装箱成long,并将执行整数相等性测试。如果我错了,请给我指出正确的方向。
yeonseok.seo

78

对于更优雅的方法,我建议两个选择:

检查返回值从insert操作与IGNORE作为OnConflictStrategy(如果它等于-1,那么意味着行未插入):

@Insert(onConflict = OnConflictStrategy.IGNORE)
long insert(Entity entity);

@Update(onConflict = OnConflictStrategy.IGNORE)
void update(Entity entity);

public void upsert(Entity entity) {
    long id = insert(entity);
    if (id == -1) {
        update(entity);   
    }
}

从处理异常insert的操作与FAILOnConflictStrategy

@Insert(onConflict = OnConflictStrategy.FAIL)
void insert(Entity entity);

@Update(onConflict = OnConflictStrategy.FAIL)
void update(Entity entity);

public void upsert(Entity entity) {
    try {
        insert(entity);
    } catch (SQLiteConstraintException exception) {
        update(entity);
    }
}

9
这对于单个实体来说效果很好,但是很难实现集合。最好过滤插入了哪些集合,并从更新中将其过滤掉。
Tunji_D

2
@DanielWilson取决于您的应用程序,此答案适用于单个实体,但是不适用于我拥有的实体列表。
Tunji_D

2
无论出于何种原因,当我执行第一种方法时,插入一个已经存在的ID都会返回一个大于存在的行号,而不是-1L。
ElliotM

41

我找不到一个不会插入或更新而不会对我的外键造成不必要更改的SQLite查询,因​​此我选择先插入,忽略发生冲突的情况,然后立即进行更新,再忽略冲突。

插入和更新方法受到保护,因此外部类只能查看和使用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);
}

6
您可能想使其更高效并检查返回值。-1表示任何形式的冲突。
jcuypers

21
最好upsert@Transaction注释标记该方法
Ohmnibus,

3
我想这样做的正确方法是询问该值是否已经在数据库上(使用其主键)。您可以在使用抽象类(更换DAO接口)或使用类做调用该对象的道
塞巴斯蒂安·科拉迪

@Ohmnibus否,因为文档说>将此批注放在Insert,Update或Delete方法上没有影响,因为它们始终在事务内运行。同样,如果用Query注释但运行了update或delete语句,则它将自动包装在事务中。 请参阅交易文档
Levon Vardanyan

1
@LevonVardanyan在链接的页面中的示例显示了与upsert非常相似的方法,其中包含插入和删除。另外,我们不是将注解放置到插入或更新中,而是放到包含两者的方法中。
Ohmnibus

8

如果表格有多个栏,您可以使用

@Insert(onConflict = OnConflictStrategy.REPLACE)

替换一行。

参考- 转到提示Android Room Codelab


18
请不要使用此方法。如果您有任何外键查看您的数据,它将触发onDelete侦听器,并且您可能不希望这样做
Alexandr Zhurkov

@AlexandrZhurkov,我想它应该仅在更新时触发,那么任何监听器(如果实现)都可以正确执行。无论如何,如果我们在数据和onDelete触发器上有监听器,则必须由代码处理
Vikas Pandey

@AlexandrZhurkov deferred = true使用外键设置实体时,此方法效果很好。
ubuntudroid

4

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

}


1
long id = insert(entity)应该为val id = insert(entity)对于Kotlin
Kibotu

@Sam,如何处理null values我不想用null更新但保留旧值的地方。?
binrebin

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

您也可以使用database.openHelper.writableDatabase.execSQL(“ SQL STATEMENT”)将@Transaction和数据库构造函数变量用于更复杂的事务


0

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

例如 :

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

0

这种陈述应该是可能的:

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

你什么意思?注释ON CONFLICT UPDATE SET a = 1, b = 2不支持Room @Query
不在
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.