Android Room Persistence Library: Upsert

96

Die Room Persistence Library von Android enthält freundlicherweise die Anmerkungen @Insert und @Update, die für Objekte oder Sammlungen funktionieren. Ich habe jedoch einen Anwendungsfall (Push-Benachrichtigungen, die ein Modell enthalten), für den ein UPSERT erforderlich wäre, da die Daten möglicherweise in der Datenbank vorhanden sind oder nicht.

Sqlite hat kein Upsert von Haus aus, und Problemumgehungen werden in dieser SO-Frage beschrieben . Wie würde man die dortigen Lösungen auf Room anwenden?

Wie kann ich eine Einfügung oder Aktualisierung in Room implementieren, die keine Fremdschlüsseleinschränkungen aufhebt? Wenn Sie insert mit onConflict = REPLACE verwenden, wird onDelete für jeden Fremdschlüssel für diese Zeile aufgerufen. In meinem Fall verursacht onDelete eine Kaskade, und das erneute Einfügen einer Zeile führt dazu, dass Zeilen in anderen Tabellen mit dem Fremdschlüssel gelöscht werden. Dies ist NICHT das beabsichtigte Verhalten.

Tunji_D
quelle

Antworten:

73

Vielleicht können Sie Ihr BaseDao so machen.

Sichern Sie den Upsert-Vorgang mit @Transaction und versuchen Sie nur zu aktualisieren, wenn das Einfügen fehlgeschlagen ist.

@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
quelle
Dies wäre schlecht für die Leistung, da es für jedes Element in der Liste mehrere Datenbankinteraktionen gibt.
Tunji_D
13
Es gibt jedoch KEIN "Einfügen in die for-Schleife".
yeonseok.seo
4
Du hast absolut recht! Ich habe das verpasst, ich dachte du fügst in die for-Schleife ein. Das ist eine großartige Lösung.
Tunji_D
2
Das ist Gold. Dies führte mich zu Florinas Beitrag, den Sie lesen sollten: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - danke für den Hinweis @ yeonseok.seo!
Benoit Duffez
1
@PRA soweit ich weiß, spielt es überhaupt keine Rolle. docs.oracle.com/javase/specs/jls/se8/html/… Long wird zu lange entpackt und ein Ganzzahl-Gleichheitstest wird durchgeführt. Bitte weisen Sie mich in die richtige Richtung, wenn ich falsch liege.
yeonseok.seo
78

Für eine elegantere Vorgehensweise würde ich zwei Optionen vorschlagen:

Überprüfen auf Rückgabewert aus der insertOperation mit IGNOREals OnConflictStrategy(wenn er gleich -1 ist, bedeutet dies, dass keine Zeile eingefügt wurde):

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

Umgang mit Ausnahme von insertBetrieb mit FAILals 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
quelle
9
Dies funktioniert gut für einzelne Entitäten, ist jedoch für eine Sammlung schwer zu implementieren. Es wäre schön zu filtern, welche Sammlungen eingefügt wurden, und sie aus dem Update herauszufiltern.
Tunji_D
2
@ DanielWilson es hängt von Ihrer Anwendung ab, diese Antwort funktioniert gut für einzelne Entitäten, sie gilt jedoch nicht für eine Liste von Entitäten, die ich habe.
Tunji_D
2
Aus welchem ​​Grund auch immer, wenn ich den ersten Ansatz mache, gibt das Einfügen einer bereits vorhandenen ID eine Zeilennummer zurück, die größer als die vorhandene ist, nicht -1L.
ElliotM
41

Ich konnte keine SQLite-Abfrage finden, die eingefügt oder aktualisiert werden konnte, ohne unerwünschte Änderungen an meinem Fremdschlüssel zu verursachen. Stattdessen habe ich mich dafür entschieden, zuerst einzufügen, Konflikte zu ignorieren, wenn sie auftraten, und unmittelbar danach zu aktualisieren und Konflikte erneut zu ignorieren.

Die Einfüge- und Aktualisierungsmethoden sind geschützt, sodass externe Klassen nur die Upsert-Methode sehen und verwenden. Beachten Sie, dass dies kein echter Upsert ist, da die MyEntity-POJOS über Nullfelder verfügen und die derzeit in der Datenbank enthaltenen Daten möglicherweise überschreiben. Dies ist keine Einschränkung für mich, aber es kann für Ihre Anwendung sein.

@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
quelle
6
Vielleicht möchten Sie es effizienter machen und nach Rückgabewerten suchen. -1 signalisiert Konflikte jeglicher Art.
Jcuypers
21
Markieren Sie die upsertMethode besser mit der @TransactionAnmerkung
Ohmnibus
3
Ich denke, der richtige Weg, dies zu tun, ist zu fragen, ob der Wert bereits in der Datenbank war (unter Verwendung seines Primärschlüssels). Sie können dies mit einer abstractClass (um die Dao-Schnittstelle zu ersetzen) oder mit der Klasse tun, die das Dao des Objekts
Sebastian Corradi
@Ohmnibus nein, da in der Dokumentation angegeben ist, dass das Hinzufügen dieser Anmerkung zu einer Einfüge-, Aktualisierungs- oder Löschmethode keine Auswirkungen hat, da sie immer innerhalb einer Transaktion ausgeführt werden. Wenn es mit Query kommentiert ist, aber eine Update- oder Löschanweisung ausführt, wird es automatisch in eine Transaktion eingeschlossen. Siehe Transaktionsdokument
Levon Vardanyan
1
@LevonVardanyan Das Beispiel auf der von Ihnen verlinkten Seite zeigt eine Methode, die Upsert sehr ähnlich ist und eine Einfügung und eine Löschung enthält. Außerdem fügen wir die Anmerkung nicht einer Einfügung oder Aktualisierung hinzu, sondern einer Methode, die beide enthält.
Ohmnibus
7

Wenn die Tabelle mehr als eine Spalte enthält, können Sie verwenden

@Insert(onConflict = OnConflictStrategy.REPLACE)

eine Zeile ersetzen.

Referenz - Gehen Sie zu den Tipps für Android Room Codelab

Vikas Pandey
quelle
18
Bitte verwenden Sie diese Methode nicht. Wenn Sie Fremdschlüssel haben, die Ihre Daten anzeigen, wird der Lösch-Listener ausgelöst, und das möchten Sie wahrscheinlich nicht
Alexandr Zhurkov,
@AlexandrZhurkov, ich denke, es sollte nur beim Update ausgelöst werden, dann würde jeder Listener, wenn dies implementiert wird, dies korrekt tun. Wie auch immer, wenn wir einen Listener für die Daten und onDelete-Trigger haben, muss dies per Code behandelt werden
Vikas Pandey,
@AlexandrZhurkov Dies funktioniert gut, wenn Sie deferred = truedie Entität mit dem Fremdschlüssel festlegen .
Ubuntudroid
4

Dies ist der Code in 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
quelle
1
long id = insert (entity) sollte val id = insert (entity) für kotlin sein
Kibotu
@ Sam, wie gehe null valuesich damit um, wo ich nicht mit null aktualisieren möchte, sondern den alten Wert beibehalten möchte. ?
Binrebin
3

Nur ein Update, wie dies gemacht werden kann, wenn Kotlin die Daten des Modells beibehält (möglicherweise, um sie wie im Beispiel in einem Zähler zu verwenden):

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

Sie können auch die Variable @Transaction und den Datenbankkonstruktor für komplexere Transaktionen verwenden, indem Sie database.openHelper.writableDatabase.execSQL ("SQL STATEMENT") verwenden.

Emirua
quelle
0

Ein anderer Ansatz, den ich mir vorstellen kann, besteht darin, die Entität per Abfrage über DAO abzurufen und dann alle gewünschten Aktualisierungen durchzuführen. Dies ist im Vergleich zu den anderen Lösungen in diesem Thread in Bezug auf die Laufzeit möglicherweise weniger effizient, da die vollständige Entität abgerufen werden muss, bietet jedoch viel mehr Flexibilität hinsichtlich der zulässigen Vorgänge, z. B. hinsichtlich der zu aktualisierenden Felder / Variablen.

Zum Beispiel :

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

Sollte mit dieser Art von Aussage möglich sein:

INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2
Brill Pappin
quelle
Was meinst du? ON CONFLICT UPDATE SET a = 1, b = 2wird von Room @QueryAnnotation nicht unterstützt .
isabsent