Libreria di persistenza della stanza Android: Upsert


98

La libreria di persistenza Room di Android include graziosamente le annotazioni @Insert e @Update che funzionano per oggetti o raccolte. Tuttavia, ho un caso d'uso (notifiche push contenenti un modello) che richiederebbe un UPSERT poiché i dati possono o non possono esistere nel database.

Sqlite non dispone di upsert in modo nativo e le soluzioni alternative sono descritte in questa domanda SO . Date le soluzioni disponibili, come applicarle a Room?

Per essere più precisi, come posso implementare un inserimento o un aggiornamento in Room che non rompa i vincoli di chiave esterna? L'utilizzo di insert con onConflict = REPLACE causerà la chiamata di onDelete per qualsiasi chiave esterna a quella riga. Nel mio caso onDelete provoca una cascata e il reinserimento di una riga causerà l'eliminazione delle righe in altre tabelle con la chiave esterna. Questo NON è il comportamento previsto.

Risposte:


77

Forse puoi rendere il tuo BaseDao così.

proteggere l'operazione upsert con @Transaction e provare ad aggiornare solo se l'inserimento non è riuscito.

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

Ciò sarebbe negativo per le prestazioni poiché ci saranno più interazioni con il database per ogni elemento nell'elenco.
Tunji_D

13
ma NON c'è "inserimento nel ciclo for".
yeonseok.seo

4
hai assolutamente ragione! Mi mancava, pensavo stessi inserendo nel ciclo for. È un'ottima soluzione.
Tunji_D

2
Questo è oro. Questo mi ha portato al post di Florina, che dovresti leggere: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - grazie per il suggerimento @ yeonseok.seo!
Benoit Duffez

1
@PRA per quanto ne so, non importa affatto. docs.oracle.com/javase/specs/jls/se8/html/… Long verrà unboxed in long e verrà eseguito il test di uguaglianza di interi. per favore indicami la giusta direzione se sbaglio.
yeonseok.seo

78

Per un modo più elegante per farlo, suggerirei due opzioni:

Verifica del valore restituito insertdall'operazione con IGNOREcome a OnConflictStrategy(se è uguale a -1, significa che la riga non è stata inserita):

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

Gestione dell'eccezione insertdall'operazione con FAILcome 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);
    }
}

9
funziona bene per le singole entità, ma è difficile da implementare per una raccolta. Sarebbe bello filtrare le raccolte che sono state inserite e filtrarle dall'aggiornamento.
Tunji_D

2
@DanielWilson dipende dalla tua applicazione, questa risposta funziona bene per singole entità, tuttavia non è applicabile per un elenco di entità che è quello che ho.
Tunji_D

2
Per qualsiasi motivo, quando eseguo il primo approccio, l'inserimento di un ID già esistente restituisce un numero di riga maggiore di quello esistente, non -1L.
ElliotM

41

Non sono riuscito a trovare una query SQLite che si inserisse o si aggiornasse senza causare modifiche indesiderate alla mia chiave esterna, quindi ho scelto di inserire prima, ignorando i conflitti se si sono verificati e aggiornando immediatamente dopo, ignorando nuovamente i conflitti.

I metodi di inserimento e aggiornamento sono protetti in modo che le classi esterne visualizzino e utilizzino solo il metodo upsert. Tieni presente che questo non è un vero upsert poiché se uno qualsiasi dei POJOS MyEntity avesse campi nulli, sovrascriveranno ciò che potrebbe essere attualmente nel database. Questo non è un avvertimento per me, ma potrebbe essere per la tua applicazione.

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

6
potresti volerlo rendere più efficiente e controllare i valori di ritorno. -1 segnala conflitti di qualsiasi tipo.
jcuypers

21
Meglio segnare il upsertmetodo con l' @Transactionannotazione
Ohmnibus

3
Immagino che il modo corretto per farlo sia chiedere se il valore era già sul DB (usando la sua chiave primaria). puoi farlo usando una abstractClass (per sostituire l'interfaccia dao) o usando la classe che chiama il dao dell'oggetto
Sebastian Corradi

@Ohmnibus no, perché la documentazione dice> Mettere questa annotazione su un metodo di inserimento, aggiornamento o eliminazione non ha alcun impatto perché vengono sempre eseguiti all'interno di una transazione. Allo stesso modo, se è annotato con Query ma esegue un'istruzione di aggiornamento o eliminazione, viene automaticamente inserito in una transazione. Vedere il documento della transazione
Levon Vardanyan

1
@LevonVardanyan l'esempio nella pagina che hai linkato mostra un metodo molto simile a upsert, contenente un inserimento e una cancellazione. Inoltre, non stiamo inserendo l'annotazione in un inserimento o aggiornamento, ma in un metodo che li contiene entrambi.
Ohmnibus

8

Se la tabella ha più di una colonna, puoi usare

@Insert(onConflict = OnConflictStrategy.REPLACE)

per sostituire una riga.

Riferimento - Vai ai suggerimenti Android Room Codelab


18
Per favore non utilizzare questo metodo. Se hai chiavi esterne che guardano i tuoi dati, si attiverà su Elimina ascoltatore e probabilmente non lo vuoi
Alexandr Zhurkov

@AlexandrZhurkov, immagino che dovrebbe attivarsi solo all'aggiornamento, quindi qualsiasi ascoltatore, se implementato, lo farebbe correttamente. Ad ogni modo, se abbiamo un listener sui dati e sui trigger di eliminazione, allora deve essere gestito dal codice
Vikas Pandey

@AlexandrZhurkov Funziona bene quando si imposta deferred = truel'entità con la chiave esterna.
ubuntudroid

4

Questo è il codice 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)
  }

}


1
long id = insert (entity) dovrebbe essere val id = insert (entity) per kotlin
Kibotu

@ Sam, come null valuescomportarmi quando non voglio aggiornare con null ma mantenere il vecchio valore. ?
binrebin

3

Solo un aggiornamento su come farlo con Kotlin mantenendo i dati del modello (forse per usarlo in un contatore come nell'esempio):

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

Puoi anche utilizzare @Transaction e la variabile di costruzione del database per transazioni più complesse utilizzando database.openHelper.writableDatabase.execSQL ("SQL STATEMENT")


0

Un altro approccio a cui posso pensare è ottenere l'entità tramite DAO tramite query e quindi eseguire gli aggiornamenti desiderati. Questo può essere meno efficiente rispetto alle altre soluzioni in questo thread in termini di runtime a causa del dover recuperare l'intera entità, ma consente molta più flessibilità in termini di operazioni consentite come su quali campi / variabili aggiornare.

Per esempio :

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

0

Dovrebbe essere possibile con questo tipo di affermazione:

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

Cosa intendi? ON CONFLICT UPDATE SET a = 1, b = 2non è supportato Room @Querydall'annotazione.
assente il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.