web-dev-qa-db-fra.com

Bibliothèque de persistance dans les salles Android: Upsert

La bibliothèque de persistance des salles d'Android inclut gracieusement les annotations @Insert et @Update qui fonctionnent pour les objets ou les collections. J'ai cependant un cas d'utilisation (notifications Push contenant un modèle) qui nécessiterait un UPSERT car les données peuvent ou non exister dans la base de données.

Sqlite n’a pas d’opération native, et des solutions de contournement sont décrites dans cette SO question . Compte tenu des solutions proposées, comment pourrait-on les appliquer à Room?

Pour être plus précis, comment puis-je implémenter une insertion ou une mise à jour dans Room qui ne briserait aucune contrainte de clé étrangère? Si vous utilisez insert avec onConflict = REPLACE, onDelete appellera toute clé étrangère de cette ligne. Dans mon cas, onDelete provoque une cascade et la réinsertion d'une ligne entraîne la suppression des lignes d'autres tables avec la clé étrangère. Ce n'est pas le comportement prévu.

41
Tunji_D

Je ne pouvais pas trouver une requête SQLite qui insère ou met à jour sans provoquer de modifications indésirables de ma clé étrangère. J'ai donc opté pour l'insertion d'abord, en ignorant les conflits s'ils se produisaient et en effectuant une mise à jour immédiatement après, en ignorant à nouveau les conflits. 

Les méthodes insert et update étant protégées, les classes externes voient et utilisent uniquement la méthode upsert. Gardez à l'esprit que ce n'est pas un véritable upsert, comme si l'un des POJOS de MyEntity avait des champs nuls, ils écraseraient ce qui pourrait se trouver dans la base de données. Ce n'est pas une mise en garde pour moi, mais peut-être pour votre application.

@Insert(onConflict = OnConflictStrategy.IGNORE)
protected abstract void insert(List<MyEntity> entities);

@Update(onConflict = OnConflictStrategy.IGNORE)
protected abstract void update(List<MyEntity> entities);

public void upsert(List<MyEntity> entities) {
    insert(models);
    update(models);
}
29
Tunji_D

Pour une manière plus élégante de le faire, je suggérerais deux options:

Vérification de la valeur de retour de l'opération insert avec IGNORE comme OnConflictStrategy (si elle est égale à -1, cela signifie que la ligne n'a pas été insérée):

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

Gestion des exceptions de l'opération insert avec FAIL en tant que 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);
    }
}
38
user3448282

Peut-être que vous pouvez faire votre BaseDao comme ça.

sécurisez l'opération upsert avec @Transaction, et essayez de mettre à jour que si l'insertion a échoué.

@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);
        }
    }
}
27
yeonseok.seo

Si la table a plus d'une colonne, vous pouvez utiliser

@Insert (onConflict = OnConflictStrategy.REPLACE)

pour remplacer une rangée. 

Référence - Allez aux conseils Android Room Codelab

4
Vikas Pandey

Juste une mise à jour pour savoir comment faire cela avec Kotlin en conservant les données du modèle (peut-être pour l'utiliser dans un compteur comme dans l'exemple):

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

Vous pouvez également utiliser @Transaction et la variable constructeur de la base de données pour des transactions plus complexes à l'aide de database.openHelper.writableDatabase.execSQL ("SQL STATEMENT").

2
emirua

Ceci est le code dans Kotlin:

@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(entity: Entity): Long

@Update(onConflict = OnConflictStrategy.REPLACE)
fun update(entity: Entity)

@Transaction
fun upsert(entity: Entity) {
  long id = insert(entity)
   if (id == -1L) {
     update(entity)
  }

}

1
Sam

Devrait être possible avec ce genre de déclaration:

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

Une autre approche à laquelle je peux penser est d’obtenir l’entité via DAO par requête, puis d’effectuer les mises à jour souhaitées. entité, mais permet beaucoup plus de flexibilité en termes d'opérations autorisées telles que les champs/variables à mettre à jour.

Par exemple :

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