Библиотека сохраняемости комнаты Android: Upsert

102

Библиотека сохраняемости Room в Android любезно включает аннотации @Insert и @Update, которые работают для объектов или коллекций. Однако у меня есть вариант использования (push-уведомления, содержащие модель), для которого потребуется UPSERT, поскольку данные могут существовать или не существовать в базе данных.

Sqlite не имеет встроенного upsert, и обходные пути описаны в этом вопросе SO . Учитывая имеющиеся там решения, как их применить к Room?

Чтобы быть более конкретным, как я могу реализовать вставку или обновление в Room, которое не нарушило бы никаких ограничений внешнего ключа? Использование вставки с onConflict = REPLACE приведет к вызову onDelete для любого внешнего ключа этой строки. В моем случае onDelete вызывает каскад, и повторная вставка строки приведет к удалению строк в других таблицах с внешним ключом. Это НЕ предполагаемое поведение.

Tunji_D
источник

Ответы:

81

Возможно, вы сможете сделать свой 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);
        }
    }
}
yeonseok.seo
источник
Это плохо сказывается на производительности, поскольку для каждого элемента в списке будет несколько взаимодействий с базой данных.
Tunji_D 07
13
но НЕТ «вставки в цикл for».
yeonseok.seo
4
вы абсолютно правы! Я пропустил это, я думал, что вы вставляете цикл for. Это отличное решение.
Tunji_D
2
Это золото. Это привело меня к сообщению Флорины, которое вы должны прочитать: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - спасибо за подсказку @ yeonseok.seo!
Benoit Duffez 01
1
@PRA, насколько я знаю, это вообще не имеет значения. docs.oracle.com/javase/specs/jls/se8/html/… Long будет распакован на long, и будет выполнена проверка на равенство целых чисел. Пожалуйста, укажите мне правильное направление, если я ошибаюсь.
yeonseok.seo
80

Для более элегантного способа сделать это я бы предложил два варианта:

Проверка возвращаемого значения из insertоперации с IGNOREas 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);
    }
}
пользователь3448282
источник
9
это хорошо работает для отдельных объектов, но сложно реализовать для коллекции. Было бы неплохо отфильтровать, какие коллекции были вставлены, и отфильтровать их из обновления.
Tunji_D
2
@DanielWilson, это зависит от вашего приложения, этот ответ хорошо работает для отдельных объектов, однако он не применим для списка объектов, который у меня есть.
Tunji_D
2
По какой-то причине, когда я использую первый подход, вставка уже существующего идентификатора возвращает номер строки, превышающий существующий, а не -1L.
ElliotM 05
1
Как сказал Омнибус в другом ответе, лучше пометьте upsertметод @Transactionаннотацией - stackoverflow.com/questions/45677230/…
Доктор Джеки
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);
}
Tunji_D
источник
6
вы можете сделать его более эффективным и проверить возвращаемые значения. -1 сигнализирует о конфликте любого рода.
jcuypers
21
Лучше отметьте upsertспособ @Transactionаннотацией
Омнибус 07
3
Я думаю, что правильный способ сделать это - спросить, было ли значение уже в БД (с использованием его первичного ключа). вы можете сделать это, используя abstractClass (чтобы заменить интерфейс dao) или используя класс, который вызывает dao объекта
Себастьян Корради
@Ohmnibus нет, потому что в документации сказано:> Помещение этой аннотации в метод Insert, Update или Delete не влияет, потому что они всегда выполняются внутри транзакции. Точно так же, если он снабжен аннотацией Query, но выполняет оператор обновления или удаления, он автоматически включается в транзакцию. См.
Левон Варданян
1
@LevonVardanyan пример на связанной странице показывает метод, очень похожий на upsert, содержащий вставку и удаление. Кроме того, мы помещаем аннотацию не к вставке или обновлению, а к методу, который содержит и то, и другое.
Ohmnibus
8

Если в таблице более одного столбца, вы можете использовать

@Insert(onConflict = OnConflictStrategy.REPLACE)

заменить строку.

Ссылка - Перейти к советам Android Room Codelab

Викас Пандей
источник
19
Пожалуйста, не используйте этот метод. Если у вас есть какие-либо внешние ключи, просматривающие ваши данные, он запустит прослушиватель onDelete, и вы, вероятно, этого не хотите
Александр Журков
@AlexandrZhurkov, я думаю, он должен срабатывать только при обновлении, тогда любой слушатель, если он будет реализован, сделает это правильно. В любом случае, если у нас есть прослушиватель данных и триггеры onDelete, тогда он должен обрабатываться с помощью кода
Викас Панди
@AlexandrZhurkov Это хорошо работает при настройке deferred = trueсущности с внешним ключом.
ubuntudroid
@ubuntudroid Это не работает даже при установке этого флага на внешнем ключе сущностей, только что протестированном. Вызов удаления по-прежнему выполняется после завершения транзакции, потому что он не отклоняется во время процесса, он просто происходит не тогда, когда это происходит, а в конце транзакции.
Shadow
4

Это код в Котлине:

@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) для котлина
Киботу
@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)
    }
}
}

Вы также можете использовать @Transaction и переменную конструктора базы данных для более сложных транзакций, используя database.openHelper.writableDatabase.execSQL («ЗАПИСЬ SQL»)

Эмируа
источник
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аннотацией.
03
-1

Если у вас есть устаревший код: некоторые сущности на Java и BaseDao as Interface(где вы не можете добавить тело функции) или вам лень заменить все implementsна extendsдля Java-потомков.

Примечание. Работает только в коде 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)
    }
}
Даниил Павленко
источник
Кажется, это ошибочно? Он не создает транзакцию должным образом.
Рава