Bibliothèque de persistance de salle Android: Upsert

98

La bibliothèque de persistance Room d'Android comprend 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'upsert nativement, et les solutions de contournement sont décrites dans cette question SO . Compte tenu des solutions, comment les appliquerait-on à 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? L'utilisation d'insert avec onConflict = REPLACE entraînera l'appel de onDelete pour toute clé étrangère de cette ligne. Dans mon cas, onDelete provoque une cascade, et la réinsertion d'une ligne entraînera la suppression de lignes dans d'autres tables avec la clé étrangère. Ce n'est PAS le comportement prévu.

Tunji_D
la source

Réponses:

78

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

sécurisez l'opération upsert avec @Transaction et essayez de mettre à jour uniquement si l'insertion échoue.

@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
la source
Cela serait mauvais pour les performances car il y aura plusieurs interactions de base de données pour chaque élément de la liste.
Tunji_D
13
mais, il n'y a PAS "d'insertion dans la boucle for".
yeonseok.seo
4
vous avez absolument raison! J'ai raté ça, je pensais que vous insériez dans la boucle for. C'est une excellente solution.
Tunji_D
2
C'est de l'or. Cela m'a conduit au post de Florina, que vous devriez lire: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - merci pour l'indice @ yeonseok.seo!
Benoit Duffez
1
@PRA pour autant que je sache, cela n'a pas d'importance du tout. docs.oracle.com/javase/specs/jls/se8/html/… Long sera déballé en long et un test d'égalité des entiers sera effectué. s'il vous plaît indiquez-moi la bonne direction si je me trompe.
yeonseok.seo
78

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

Vérification de la valeur de retour de l' insertopération avec IGNOREas a 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);   
    }
}

Traitement de l'exception d' insertopération avec FAILcomme 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);
    }
}
user3448282
la source
9
cela fonctionne bien pour les entités individuelles, mais est difficile à implémenter pour une collection. Ce serait bien de filtrer les collections insérées et de les exclure de la mise à jour.
Tunji_D
2
@DanielWilson cela dépend de votre application, cette réponse fonctionne bien pour des entités uniques, mais elle n'est pas applicable pour une liste d'entités qui est ce que j'ai.
Tunji_D
2
Pour une raison quelconque, lorsque je fais la première approche, l'insertion d'un ID déjà existant renvoie un numéro de ligne supérieur à ce qui existe, pas -1L.
ElliotM
41

Je n'ai pas pu trouver de requête SQLite qui s'insérerait ou se mettrait à jour sans provoquer de modifications indésirables de ma clé étrangère.J'ai donc choisi d'insérer d'abord, en ignorant les conflits s'ils se produisaient et en mettant à jour immédiatement après, en ignorant à nouveau les conflits.

Les méthodes d'insertion et de mise à jour sont protégées afin que les classes externes voient et utilisent uniquement la méthode upsert. Gardez à l'esprit que ce n'est pas un véritable upsert, car si l'un des POJOS MyEntity avait des champs nuls, ils écraseraient ce qui peut actuellement 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);

@Transaction
public void upsert(List<MyEntity> entities) {
    insert(models);
    update(models);
}
Tunji_D
la source
6
vous voudrez peut-être le rendre plus efficace et vérifier les valeurs de retour. -1 signale un conflit de quelque nature que ce soit.
jcuypers
21
Mieux vaut marquer la upsertméthode avec l' @Transactionannotation
Ohmnibus
3
Je suppose que la bonne façon de procéder consiste à demander si la valeur était déjà sur la base de données (en utilisant sa clé primaire). vous pouvez le faire en utilisant une abstractClass (pour remplacer l'interface dao) ou en utilisant la classe qui appelle le dao de l'objet
Sebastian Corradi
@Ohmnibus non, car la documentation dit> Mettre cette annotation sur une méthode d'insertion, de mise à jour ou de suppression n'a aucun impact car elles sont toujours exécutées dans une transaction. De même, s'il est annoté avec Query mais exécute une instruction de mise à jour ou de suppression, il est automatiquement encapsulé dans une transaction. Voir le document de transaction
Levon Vardanyan
1
@LevonVardanyan l'exemple de la page que vous avez liée montre une méthode très similaire à upsert, contenant une insertion et une suppression. De plus, nous ne mettons pas l'annotation à une insertion ou une mise à jour, mais à une méthode qui contient les deux.
Ohmnibus
8

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

@Insert(onConflict = OnConflictStrategy.REPLACE)

pour remplacer une ligne.

Référence - Aller aux astuces Android Room Codelab

Vikas Pandey
la source
18
Veuillez ne pas utiliser cette méthode. Si vous avez des clés étrangères regardant vos données, cela déclenchera l'auditeur onDelete et vous ne le voulez probablement pas
Alexandr Zhurkov
@AlexandrZhurkov, je suppose que cela ne devrait se déclencher que lors de la mise à jour, alors tout auditeur, s'il était mis en œuvre, le ferait correctement. Quoi qu'il en soit, si nous avons un auditeur sur les déclencheurs de données et onDelete, il doit être géré par code
Vikas Pandey
@AlexandrZhurkov Cela fonctionne bien lors de la définition deferred = truede l'entité avec la clé étrangère.
ubuntudroid le
4

Voici le code de 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
la source
1
long id = insert (entity) devrait être val id = insert (entity) for kotlin
Kibotu
@Sam, comment gérer les cas null valuesoù je ne veux pas mettre à jour avec null mais conserver l'ancienne valeur. ?
binrebin
3

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 de constructeur de base de données pour des transactions plus complexes en utilisant database.openHelper.writableDatabase.execSQL ("SQL STATEMENT")

émirua
la source
0

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. Cela peut être moins efficace par rapport aux autres solutions de ce thread en termes d'exécution en raison de la nécessité de récupérer l'entité complète, mais permet beaucoup plus de flexibilité en termes d'opérations autorisées telles que sur 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);
   }
}
atjua
la source
0

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

INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2
Brill Pappin
la source
Que voulez-vous dire? ON CONFLICT UPDATE SET a = 1, b = 2n'est pas pris en charge par l' Room @Queryannotation.
isabsent