Бібліотека стійкості Android Room: Upsert


102

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

Sqlite спочатку не підтримує підтримку, і обхідні шляхи описані в цьому запитанні SO . З огляду на тамтешні рішення, як застосувати їх до Room?

Якщо бути більш конкретним, як я можу застосувати вставку або оновлення в кімнаті, що не порушить жодних обмежень зовнішнього ключа? Використання вставки з onConflict = REPLACE призведе до виклику onDelete будь-якого зовнішнього ключа цього рядка. У моєму випадку onDelete викликає каскад, а повторне вставлення рядка призведе до видалення рядків в інших таблицях із зовнішнім ключем. Це НЕ передбачувана поведінка.

Відповіді:


80

Можливо, ви можете зробити свій 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);
        }
    }
}

Це буде погано для продуктивності, оскільки для кожного елемента у списку буде багато взаємодій баз даних.
Tunji_D

13
але "Вставити в цикл for" НІЯКОГО.
yeonseok.seo

4
Ви абсолютно праві! Я пропустив це, я думав, ви вставляєте в цикл for. Це чудове рішення.
Tunji_D

2
Це золото. Це призвело мене до публікації Флоріни , яку ви повинні прочитати: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - дякую за підказку @ yeonseok.seo!
Бенуа Даффес

1
@PRA, наскільки мені відомо, це взагалі не має значення. docs.oracle.com/javase/specs/jls/se8/html/... Long буде розпаковано в long і буде проведено тест цілої рівності. будь ласка, вкажіть мені правильний напрямок, якщо я помиляюся.
yeonseok.seo

80

Для більш елегантного способу цього я б запропонував два варіанти:

Перевірка поверненого значення з insertоперації з IGNOREяк a 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операції з FAILяк OnConflictStrategy:

@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
З будь-якої причини, коли я роблю перший підхід, вставка вже існуючого ідентифікатора повертає номер рядка, більший за існуючий, а не -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анотацією
Омнібус

3
Я думаю, правильний спосіб зробити це - запитати, чи значення вже було в БД (за допомогою його первинного ключа). це можна зробити за допомогою abstractClass (для заміни інтерфейсу dao) або за допомогою класу, який викликає дао об’єкта
Себастьян Корраді

@Ohmnibus ні, оскільки в документації сказано> Поміщення цієї анотації на метод Insert, Update або Delete не впливає, оскільки вони завжди запускаються всередині транзакції. Подібним чином, якщо він анотований Query, але запускає оператор оновлення або видалення, він автоматично загортається в транзакцію. Див. Документ Transaction
Левон Варданян,

1
@LevonVardanyan на прикладі сторінки, на яку ви зв’язали, показано метод, дуже схожий на upsert, що містить вставку та видалення. Крім того, ми ставимо анотацію не до вставки чи оновлення, а до методу, що містить обидва.
Омнібус

8

Якщо таблиця містить більше одного стовпця, ви можете використовувати

@Insert(onConflict = OnConflictStrategy.REPLACE)

для заміни ряду.

Довідково - Перейдіть до порад Android Room Codelab


19
Будь ласка, не використовуйте цей метод. Якщо у вас є якісь зовнішні ключі, які переглядають ваші дані, це спрацьовує на Видалити слухач, і ви, мабуть, цього не хочете
Олександр

@AlexandrZhurkov, я думаю, це повинно спрацьовувати лише при оновленні, тоді будь-який слухач, якщо це реалізується, зробить це правильно. У будь-якому випадку, якщо у нас є прослуховувач даних та тригери onDelete, тоді він повинен оброблятися кодом
Vikas Pandey

@AlexandrZhurkov Це добре працює при налаштуванні deferred = trueсутності із зовнішнім ключем.
ubuntudroid

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

Ви також можете використовувати змінну @Transaction і конструктор бази даних для більш складних транзакцій, використовуючи database.openHelper.writableDatabase.execSQL ("SQL STATEMENT")


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анотацією.
відсутній

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

Здається, це недоліки? Це неправильно створює транзакцію.
Рава,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.