Library Persistensi Ruang Android: Upsert

102

Library persistensi Room Android dengan anggun menyertakan anotasi @Insert dan @Update yang berfungsi untuk objek atau koleksi. Namun saya memiliki kasus penggunaan (pemberitahuan push yang berisi model) yang akan memerlukan UPSERT karena datanya mungkin ada atau mungkin tidak ada dalam database.

Sqlite tidak memiliki upsert secara asli, dan penyelesaiannya dijelaskan dalam pertanyaan SO ini . Mengingat solusi yang ada, bagaimana cara menerapkannya ke Room?

Untuk lebih spesifiknya, bagaimana cara menerapkan penyisipan atau pembaruan di Room yang tidak akan merusak batasan kunci asing? Menggunakan sisipkan dengan onConflict = REPLACE akan menyebabkan onDelete untuk kunci asing apa pun ke baris itu dipanggil. Dalam kasus saya onDelete menyebabkan kaskade, dan memasukkan kembali baris akan menyebabkan baris di tabel lain dengan kunci asing dihapus. Ini BUKAN perilaku yang diinginkan.

Tunji_D
sumber

Jawaban:

81

Mungkin kamu bisa menjadikan BaseDao seperti ini.

amankan operasi upsert dengan @Transaction, dan coba perbarui hanya jika penyisipan gagal.

@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
sumber
Ini akan berdampak buruk bagi kinerja karena akan ada beberapa interaksi basis data untuk setiap elemen dalam daftar.
Tunji_D
13
tetapi, TIDAK ada "sisipkan di loop for".
yeonseok.seo
4
Anda memang benar! Saya melewatkan itu, saya pikir Anda memasukkan for loop. Itu solusi yang bagus.
Tunji_D
2
Ini emas. Ini membawa saya ke posting Florina, yang harus Anda baca: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - terima kasih atas petunjuknya @ yeonseok.seo!
Benoit Duffez
1
@PRA sejauh yang saya tahu, tidak masalah sama sekali. docs.oracle.com/javase/specs/jls/se8/html/… Long akan dibuka kotaknya ke long dan uji persamaan integer akan dilakukan. tolong arahkan saya ke arah yang benar jika saya salah.
yeonseok.seo
80

Untuk cara yang lebih elegan untuk melakukannya, saya menyarankan dua opsi:

Memeriksa nilai kembali dari insertoperasi dengan IGNOREsebagai OnConflictStrategy(jika sama dengan -1 maka itu berarti baris tidak dimasukkan):

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

Menangani pengecualian dari insertoperasi dengan FAILsebagai 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);
    }
}
pengguna3448282
sumber
9
ini berfungsi dengan baik untuk masing-masing entitas, tetapi sulit diterapkan untuk koleksi. Alangkah baiknya untuk memfilter koleksi apa yang dimasukkan dan memfilternya dari pembaruan.
Tunji_D
2
@DanielWilson tergantung pada aplikasi Anda, jawaban ini berfungsi dengan baik untuk entitas tunggal, namun tidak berlaku untuk daftar entitas yang saya miliki.
Tunji_D
2
Untuk alasan apa pun, ketika saya melakukan pendekatan pertama, memasukkan ID yang sudah ada mengembalikan nomor baris lebih besar dari yang ada, bukan -1L.
ElliotM
1
Seperti yang dikatakan Ohmnibus di jawaban lain, lebih baik tandai upsertmetode tersebut dengan @Transactionpenjelasan - stackoverflow.com/questions/45677230/…
Dr.jacky
41

Saya tidak dapat menemukan kueri SQLite yang akan menyisipkan atau memperbarui tanpa menyebabkan perubahan yang tidak diinginkan pada kunci asing saya, jadi saya memilih untuk menyisipkan terlebih dahulu, mengabaikan konflik jika terjadi, dan memperbarui segera setelah itu, lagi-lagi mengabaikan konflik.

Metode penyisipan dan pembaruan dilindungi sehingga kelas eksternal hanya melihat dan menggunakan metode upsert. Perlu diingat bahwa ini bukan upert yang sebenarnya karena jika salah satu POJOS MyEntity memiliki bidang nol, mereka akan menimpa apa yang saat ini mungkin ada di database. Ini bukan peringatan bagi saya, tetapi mungkin untuk aplikasi Anda.

@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
sumber
6
Anda mungkin ingin membuatnya lebih efisien dan memeriksa nilai yang dikembalikan. -1 sinyal konflik apapun.
jcuypers
21
Lebih baik tandai upsertmetode dengan @Transactionanotasi
Ohmnibus
3
Saya kira cara yang tepat untuk melakukan ini adalah menanyakan apakah nilainya sudah ada di DB (menggunakan kunci utamanya). Anda dapat melakukannya dengan menggunakan abstractClass (untuk menggantikan antarmuka dao) atau menggunakan kelas yang memanggil dao objek
Sebastian Corradi
@Ohmnibus no, karena dokumentasinya mengatakan> Menempatkan anotasi ini pada metode Sisipkan, Perbarui, atau Hapus tidak berdampak karena selalu dijalankan di dalam transaksi. Demikian pula, jika itu dianotasi dengan Query tetapi menjalankan update atau delete statement, secara otomatis dibungkus dalam sebuah transaksi. Lihat dokumen Transaksi
Levon Vardanyan
1
@LevonVardanyan contoh di laman yang Anda tautkan menunjukkan metode yang sangat mirip dengan upsert, berisi penyisipan dan penghapusan. Selain itu, kami tidak memasukkan anotasi ke penyisipan atau pembaruan, tetapi ke metode yang berisi keduanya.
Ohmnibus
8

Jika tabel memiliki lebih dari satu kolom, Anda dapat menggunakan

@Insert(onConflict = OnConflictStrategy.REPLACE)

untuk mengganti baris.

Referensi - Buka tips Android Room Codelab

Vikas Pandey
sumber
19
Tolong jangan gunakan metode ini. Jika Anda memiliki kunci asing yang melihat data Anda, itu akan memicu pendengar onDelete dan Anda mungkin tidak menginginkannya
Alexandr Zhurkov
@AlexandrZhurkov, saya kira itu harus memicu hanya pada pembaruan, maka pendengar jika diterapkan ini akan melakukannya dengan benar. Pokoknya jika kita memiliki pendengar pada data dan pemicu onDelete maka itu harus ditangani oleh kode
Vikas Pandey
@AlexandrZhurkov Ini bekerja dengan baik saat mengatur deferred = trueentitas dengan kunci asing.
ubuntudroid
@ubuntudroid Itu tidak bekerja dengan baik bahkan ketika mengatur bendera itu pada kunci asing entitas, baru saja diuji. Panggilan hapus masih berjalan setelah transaksi selesai karena tidak diberhentikan selama proses, itu hanya tidak terjadi ketika itu terjadi tetapi di akhir transaksi masih.
Bayangan
4

Ini adalah kode di 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
sumber
1
long id = insert (entity) harus val id = insert (entity) untuk kotlin
Kibotu
@ Sam, bagaimana menangani di null valuesmana saya tidak ingin memperbarui dengan null tetapi mempertahankan nilai lama. ?
binrebin
3

Hanya pembaruan tentang cara melakukan ini dengan Kotlin yang menyimpan data model (Mungkin menggunakannya di penghitung seperti pada contoh):

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

Anda juga dapat menggunakan @Transaction dan variabel konstruktor database untuk transaksi yang lebih kompleks menggunakan database.openHelper.writableDatabase.execSQL ("SQL STATEMENT")

emirua
sumber
0

Pendekatan lain yang dapat saya pikirkan adalah mendapatkan entitas melalui DAO menurut kueri, lalu melakukan pembaruan yang diinginkan. Ini mungkin kurang efisien dibandingkan dengan solusi lain di thread ini dalam hal runtime karena harus mengambil entitas penuh, tetapi memungkinkan lebih banyak fleksibilitas dalam hal operasi yang diizinkan seperti pada bidang / variabel apa yang akan diperbarui.

Sebagai contoh :

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

Seharusnya mungkin dengan pernyataan semacam ini:

INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2
Brill Pappin
sumber
Apa maksudmu? ON CONFLICT UPDATE SET a = 1, b = 2tidak didukung oleh Room @Queryanotasi.
berlaku pada
-1

Jika Anda memiliki kode warisan: beberapa entitas di Java dan BaseDao as Interface(di mana Anda tidak dapat menambahkan badan fungsi) atau Anda terlalu malas untuk mengganti semua implementsdengan extendsuntuk Java-anak.

Catatan: Ini hanya berfungsi di kode Kotlin. Saya yakin Anda menulis kode baru di Kotlin, saya benar? :)

Akhirnya solusi malas adalah menambahkan dua 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
sumber
Sepertinya ini cacat? Itu tidak membuat transaksi dengan benar.
Rawa