Biblioteca de persistencia de salas de Android: Upsert

102

La biblioteca de persistencia Room de Android incluye gentilmente las anotaciones @Insert y @Update que funcionan para objetos o colecciones. Sin embargo, tengo un caso de uso (notificaciones push que contienen un modelo) que requeriría un UPSERT, ya que los datos pueden o no existir en la base de datos.

Sqlite no tiene upsert de forma nativa, y las soluciones se describen en esta pregunta SO . Dadas las soluciones allí, ¿cómo se las aplicaría a Room?

Para ser más específico, ¿cómo puedo implementar una inserción o actualización en Room que no rompa ninguna restricción de clave externa? El uso de insert con onConflict = REPLACE hará que se llame a onDelete para cualquier clave externa a esa fila. En mi caso, onDelete provoca una cascada, y reinsertar una fila hará que las filas en otras tablas con la clave externa se eliminen. Este NO es el comportamiento previsto.

Tunji_D
fuente

Respuestas:

81

Quizás puedas hacer tu BaseDao así.

asegure la operación upsert con @Transaction e intente actualizar solo si falla la inserción.

@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
fuente
Esto sería perjudicial para el rendimiento, ya que habrá múltiples interacciones de base de datos para cada elemento de la lista.
Tunji_D
13
pero, NO hay "insertar en el bucle for".
yeonseok.seo
4
¡Estás absolutamente en lo correcto! Me perdí eso, pensé que estabas insertando en el bucle for. Esa es una gran solucion.
Tunji_D
2
Esto es oro. Esto me llevó a la publicación de Florina, que debería leer: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - ¡gracias por la pista @ yeonseok.seo!
Benoit Duffez
1
@PRA que yo sepa, no importa en absoluto. docs.oracle.com/javase/specs/jls/se8/html/… Long se descompondrá en long y se realizará una prueba de igualdad de enteros. por favor, indíqueme la dirección correcta si me equivoco.
yeonseok.seo
80

Para una forma más elegante de hacerlo, sugeriría dos opciones:

Comprobando el valor de retorno de la insertoperación con IGNOREcomo OnConflictStrategy(si es igual a -1, significa que la fila no se insertó):

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

Manejo de la excepción de insertoperación con FAILcomo 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);
    }
}
usuario3448282
fuente
9
esto funciona bien para entidades individuales, pero es difícil de implementar para una colección. Sería bueno filtrar qué colecciones se insertaron y filtrarlas de la actualización.
Tunji_D
2
@DanielWilson depende de su aplicación, esta respuesta funciona bien para entidades individuales, sin embargo, no es aplicable para una lista de entidades, que es lo que tengo.
Tunji_D
2
Por la razón que sea, cuando hago el primer enfoque, insertar una ID ya existente devuelve un número de fila mayor que el existente, no -1L.
ElliotM
1
Como dijo Ohmnibus en la otra respuesta, mejor marque el upsertmétodo con la @Transactionanotación - stackoverflow.com/questions/45677230/…
Dr.jacky
41

No pude encontrar una consulta SQLite que se insertara o actualizara sin causar cambios no deseados en mi clave externa, así que opté por insertar primero, ignorando los conflictos si ocurrieron y actualizándolos inmediatamente después, nuevamente ignorando los conflictos.

Los métodos de inserción y actualización están protegidos, por lo que las clases externas solo ven y usan el método upsert. Tenga en cuenta que esto no es una verdadera afirmación, ya que si alguno de los POJOS de MyEntity tiene campos nulos, sobrescribirán lo que puede estar actualmente en la base de datos. Esto no es una advertencia para mí, pero puede serlo para su aplicación.

@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
fuente
6
es posible que desee hacerlo más eficiente y verificar los valores de retorno. -1 indica conflicto de cualquier tipo.
jcuypers
21
Mejor marque el upsertmétodo con la @Transactionanotación
Ohmnibus
3
Supongo que la forma correcta de hacer esto es preguntando si el valor ya estaba en la base de datos (usando su clave principal). puede hacer eso usando un abstractClass (para reemplazar la interfaz dao) o usando la clase que llama al dao del objeto
Sebastian Corradi
@Ohmnibus no, porque la documentación dice> Poner esta anotación en un método Insertar, Actualizar o Eliminar no tiene ningún impacto porque siempre se ejecutan dentro de una transacción. De manera similar, si está anotado con Consulta pero ejecuta una declaración de actualización o eliminación, se incluye automáticamente en una transacción. Ver documento de transacción
Levon Vardanyan
1
@LevonVardanyan el ejemplo en la página que vinculó muestra un método muy similar a upsert, que contiene una inserción y una eliminación. Además, no colocamos la anotación en una inserción o actualización, sino en un método que contiene ambos.
Ohmnibus
8

Si la tabla tiene más de una columna, puede usar

@Insert(onConflict = OnConflictStrategy.REPLACE)

para reemplazar una fila.

Referencia: ir a los consejos de Android Room Codelab

Vikas Pandey
fuente
19
No utilice este método. Si tiene claves externas mirando sus datos, se activará en Eliminar oyente y probablemente no lo desee
Alexandr Zhurkov
@AlexandrZhurkov, supongo que debería activarse solo en la actualización, entonces cualquier oyente, si se implementa, lo haría correctamente. De todos modos, si tenemos un oyente en los datos y activadores onDelete, entonces debe ser manejado por código
Vikas Pandey
@AlexandrZhurkov Esto funciona bien cuando se configura deferred = truela entidad con la clave externa.
ubuntudroid
@ubuntudroid No funciona bien incluso cuando se configura esa bandera en la clave externa de la entidad, recién probada. La llamada de eliminación aún se realiza una vez que se completan las transacciones porque no se descarta durante el proceso, simplemente no ocurre cuando sucede, sino al final de la transacción.
Shadow
4

Este es el código en 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)
  }
}
Sam
fuente
1
long id = insertar (entidad) debe ser val id = insertar (entidad) para kotlin
Kibotu
@Sam, cómo lidiar con null valueslo que no quiero actualizar con nulo pero conservar el valor anterior. ?
Binrebin
3

Solo una actualización de cómo hacer esto con Kotlin reteniendo datos del modelo (tal vez para usarlo en un contador como en el ejemplo):

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

También puede usar @Transaction y la variable constructora de la base de datos para transacciones más complejas usando database.openHelper.writableDatabase.execSQL ("SQL STATEMENT")

emirua
fuente
0

Otro enfoque en el que puedo pensar es obtener la entidad a través de DAO mediante consulta y luego realizar las actualizaciones deseadas. Esto puede ser menos eficiente en comparación con las otras soluciones en este hilo en términos de tiempo de ejecución debido a tener que recuperar la entidad completa, pero permite mucha más flexibilidad en términos de operaciones permitidas, como qué campos / variable actualizar.

Por ejemplo :

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

Debería ser posible con este tipo de declaración:

INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2
Brill Pappin
fuente
¿Qué quieres decir? ON CONFLICT UPDATE SET a = 1, b = 2no es compatible con la Room @Queryanotación.
ausente el
-1

Si tiene código heredado: algunas entidades en Java y BaseDao as Interface(donde no puede agregar un cuerpo de función) o es demasiado vago para reemplazar todo implementscon extendsJava-children.

Nota: funciona solo en código Kotlin. Estoy seguro de que escribe código nuevo en Kotlin, ¿verdad? :)

Finalmente, una solución perezosa es agregar dos 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)
    }
}
Daniil Pavlenko
fuente
¿Parece que esto tiene fallas? No crea correctamente una transacción.
Rawa