Biblioteka trwałości pomieszczeń Androida: Upsert

102

Biblioteka trwałości Room systemu Android łaskawie zawiera adnotacje @Insert i @Update, które działają dla obiektów lub kolekcji. Mam jednak przypadek użycia (powiadomienia push zawierające model), który wymagałby przesłania UPSERT, ponieważ dane mogą, ale nie muszą istnieć w bazie danych.

Sqlite nie ma natywnego upsert, a obejścia są opisane w tym pytaniu SO . Biorąc pod uwagę dostępne tam rozwiązania, jak można je zastosować do Pokoju?

Mówiąc dokładniej, w jaki sposób mogę zaimplementować wstawianie lub aktualizację w pokoju, która nie złamałaby żadnych ograniczeń klucza obcego? Użycie insert z onConflict = REPLACE spowoduje wywołanie onDelete dla dowolnego klucza obcego w tym wierszu. W moim przypadku onDelete powoduje kaskadę, a ponowne wstawienie wiersza spowoduje usunięcie wierszy w innych tabelach z kluczem obcym. NIE jest to zamierzone zachowanie.

Tunji_D
źródło

Odpowiedzi:

81

Być może uda Ci się w ten sposób stworzyć swoją Bazę Dao

zabezpiecz operację upsert za pomocą @Transaction i spróbuj zaktualizować tylko wtedy, gdy wstawienie się nie powiedzie.

@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
źródło
Byłoby to niekorzystne dla wydajności, ponieważ dla każdego elementu listy będzie występować wiele interakcji z bazą danych.
Tunji_D
13
ale nie ma „wstawiania do pętli for”.
yeonseok.seo
4
masz całkowitą rację! Brakowało mi tego, myślałem, że wstawiasz pętlę for. To świetne rozwiązanie.
Tunji_D,
2
To jest złoto. To doprowadziło mnie do postu Floriny, który powinieneś przeczytać: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - dzięki za podpowiedź @ yeonseok.seo!
Benoit Duffez
1
O ile wiem, @PRA nie ma to żadnego znaczenia. docs.oracle.com/javase/specs/jls/se8/html/… Long zostanie rozpakowany do long i zostanie przeprowadzony test równości liczb całkowitych. proszę, wskaż mi właściwy kierunek, jeśli się mylę.
yeonseok.seo
80

Dla bardziej eleganckiego sposobu, sugerowałbym dwie opcje:

Sprawdzanie zwracanej wartości z insertoperacji IGNOREjako a OnConflictStrategy(jeśli jest równa -1, oznacza to, że wiersz nie został wstawiony):

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

Obsługa wyjątku od insertoperacji FAILjako OnConflictStrategy:

@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);
    }
}
user3448282
źródło
9
działa to dobrze w przypadku pojedynczych jednostek, ale jest trudne do zaimplementowania w przypadku kolekcji. Fajnie byłoby przefiltrować, jakie kolekcje zostały wstawione i odfiltrować je z aktualizacji.
Tunji_D
2
@DanielWilson to zależy od twojej aplikacji, ta odpowiedź działa dobrze dla pojedynczych podmiotów, jednak nie ma zastosowania do listy podmiotów, którą mam.
Tunji_D
2
Z jakiegoś powodu, kiedy wykonuję pierwsze podejście, wstawienie już istniejącego identyfikatora zwraca numer wiersza większy niż istniejący, a nie -1L.
ElliotM,
1
Jak Ohmnibus powiedział przy drugiej odpowiedzi, lepiej oznacz upsertmetodę @Transactionadnotacją - stackoverflow.com/questions/45677230/…
Dr.jacky
41

Nie mogłem znaleźć zapytania SQLite, które wstawiłoby lub zaktualizowałoby się bez powodowania niechcianych zmian w moim kluczu obcym, więc zamiast tego zdecydowałem się wstawić najpierw, ignorując konflikty, jeśli wystąpią, i aktualizując natychmiast po tym, ponownie ignorując konflikty.

Metody insert i update są chronione, więc klasy zewnętrzne widzą i używają tylko metody upsert. Należy pamiętać, że to nie jest prawdziwy upsert, ponieważ którykolwiek z MyEntity POJOS ma puste pola, nadpisze to, co może obecnie znajdować się w bazie danych. Nie jest to dla mnie zastrzeżenie, ale może być związane z Twoją aplikacją.

@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
źródło
6
możesz chcieć zwiększyć wydajność i sprawdzić zwracane wartości. -1 oznacza konflikt dowolnego rodzaju.
jcuypers
21
Lepiej oznacz upsertmetodę z @Transactionadnotacją
Ohmnibus
3
Myślę, że właściwym sposobem na zrobienie tego jest pytanie, czy wartość była już w bazie danych (przy użyciu klucza podstawowego). możesz to zrobić używając abstractClass (aby zamienić interfejs dao) lub używając klasy, która wywołuje dao obiektu
Sebastian Corradi
@Ohmnibus nie, ponieważ dokumentacja mówi:> Umieszczenie tej adnotacji w metodzie Insert, Update lub Delete nie ma żadnego wpływu, ponieważ są one zawsze uruchamiane wewnątrz transakcji. Podobnie, jeśli ma adnotację Query, ale uruchamia instrukcję aktualizacji lub usunięcia, jest automatycznie pakowana w transakcję. Zobacz dokument transakcyjny
Levon Vardanyan,
1
@LevonVardanyan przykład na stronie, do której prowadzi łącze, przedstawia metodę bardzo podobną do upsert, zawierającą wstawianie i usuwanie. Ponadto nie umieszczamy adnotacji we wstawce lub aktualizacji, ale w metodzie, która zawiera oba.
Ohmnibus
8

Jeśli tabela ma więcej niż jedną kolumnę, możesz użyć

@Insert(onConflict = OnConflictStrategy.REPLACE)

zastąpić wiersz.

Odniesienie - Przejdź do wskazówek Android Room Codelab

Vikas Pandey
źródło
19
Nie używaj tej metody. Jeśli masz jakieś klucze obce przeglądające swoje dane, uruchomi się onDelete listener i prawdopodobnie tego nie chcesz
Alexandr Zhurkov
@AlexandrZhurkov, wydaje mi się, że powinno to uruchamiać się tylko przy aktualizacji, a następnie każdy słuchacz, jeśli zostanie zaimplementowany, zrobi to poprawnie. W każdym razie, jeśli mamy słuchacza na danych i na wyzwalaczach onDelete, to musi to być obsługiwane przez kod
Vikas Pandey
@AlexandrZhurkov Działa to dobrze przy ustawianiu deferred = truejednostki z kluczem obcym.
ubuntudroid
@ubuntudroid Nie działa dobrze nawet po ustawieniu tej flagi na kluczu obcym jednostki, właśnie przetestowanym. Wywołanie usuwania nadal przechodzi po zakończeniu transakcji, ponieważ nie jest odrzucane podczas procesu, po prostu nie występuje, gdy to się dzieje, ale nadal na końcu transakcji.
Shadow
4

Oto kod w Kotlinie:

@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
źródło
1
long id = insert (entity) should be val id = insert (entity) for kotlin
Kibotu
@Sam, jak radzić sobie z sytuacją, w null valuesktórej nie chcę aktualizować z wartością null, ale zachowuję starą wartość. ?
binrebin
3

Tylko aktualizacja, jak to zrobić z Kotlinem zachowującym dane modelu (może użyć go w liczniku, jak na przykładzie):

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

Możesz również użyć @Transaction i zmiennej konstruktora bazy danych dla bardziej złożonych transakcji, używając database.openHelper.writableDatabase.execSQL („SQL STATEMENT”)

emirua
źródło
0

Innym podejściem, które przychodzi mi do głowy, jest pobranie jednostki za pośrednictwem DAO za pomocą zapytania, a następnie wykonanie żądanych aktualizacji. Może to być mniej wydajne w porównaniu z innymi rozwiązaniami w tym wątku pod względem czasu wykonywania z powodu konieczności pobierania całej jednostki, ale pozwala na znacznie większą elastyczność w zakresie operacji dozwolonych, takich jak pola / zmienne do aktualizacji.

Na przykład :

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

Powinno to być możliwe z takim stwierdzeniem:

INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2
Brill Pappin
źródło
Co masz na myśli? ON CONFLICT UPDATE SET a = 1, b = 2nie jest obsługiwany przez Room @Queryadnotację.
nieobecny
-1

Jeśli masz starszych kod: Niektóre jednostki w Javie i BaseDao as Interface(gdzie nie można dodać ciało funkcji) albo zbyt leniwy na zastąpienie wszystkich implementsz extendsJava-dzieci.

Uwaga: działa tylko w kodzie Kotlin. Jestem pewien, że piszesz nowy kod w Kotlinie, mam rację? :)

Wreszcie leniwym rozwiązaniem jest dodanie dwóch 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)
    }
}
Daniil Pavlenko
źródło
Wygląda na to, że jest wadliwy? Nie tworzy prawidłowo transakcji.
Rawa