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.
Para una forma más elegante de hacerlo, sugeriría dos opciones:
Comprobando el valor de retorno de la
insert
operación conIGNORE
comoOnConflictStrategy
(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
insert
operación conFAIL
comoOnConflictStrategy
:@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); } }
fuente
upsert
método con la@Transaction
anotación - stackoverflow.com/questions/45677230/…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); }
fuente
upsert
método con la@Transaction
anotaciónSi 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
fuente
deferred = true
la entidad con la clave externa.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) } }
fuente
null values
lo que no quiero actualizar con nulo pero conservar el valor anterior. ?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")
fuente
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); } }
fuente
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
fuente
ON CONFLICT UPDATE SET a = 1, b = 2
no es compatible con laRoom
@Query
anotación.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 todoimplements
conextends
Java-children.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) } }
fuente