ห้องสมุดความคงอยู่ของห้อง Android: อัปเดต


106

ไลบรารีความคงอยู่ของ Room ของ Android มีคำอธิบายประกอบ @Insert และ @Update ที่ใช้กับวัตถุหรือคอลเล็กชัน อย่างไรก็ตามฉันมีกรณีการใช้งาน (การแจ้งเตือนแบบพุชที่มีโมเดล) ที่ต้องใช้ UPSERT เนื่องจากข้อมูลอาจมีหรือไม่มีอยู่ในฐานข้อมูล

Sqlite ไม่มีการเพิ่มขึ้นโดยกำเนิดและวิธีแก้ปัญหาจะอธิบายไว้ในคำถาม SOนี้ จากวิธีแก้ปัญหาที่นั่นเราจะนำไปใช้กับ Room ได้อย่างไร

เพื่อให้เจาะจงมากขึ้นฉันจะใช้การแทรกหรือการอัปเดตในห้องที่ไม่ทำลายข้อ จำกัด ของคีย์แปลกปลอมได้อย่างไร การใช้การแทรกกับ onConflict = REPLACE จะทำให้ onDelete สำหรับคีย์ต่างประเทศใด ๆ ในแถวนั้นถูกเรียก ในกรณีของฉัน onDelete ทำให้เกิดการเรียงซ้อนและการใส่แถวใหม่จะทำให้แถวในตารางอื่นที่มีคีย์นอกนั้นถูกลบ นี่ไม่ใช่พฤติกรรมที่ตั้งใจไว้

คำตอบ:


84

บางทีคุณอาจทำให้ BaseDao เป็นแบบนี้ก็ได้

รักษาความปลอดภัยของการดำเนินการเพิ่มด้วย @Transaction และพยายามอัปเดตเฉพาะเมื่อการแทรกล้มเหลว

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

ซึ่งจะไม่ดีต่อประสิทธิภาพเนื่องจากจะมีการโต้ตอบกับฐานข้อมูลหลายรายการสำหรับทุกองค์ประกอบในรายการ
Tunji_D

13
แต่ไม่มี "แทรกในสำหรับลูป"
yeonseok.seo

4
คุณพูดถูกจริงๆ! ฉันพลาดสิ่งนั้นฉันคิดว่าคุณกำลังแทรกสำหรับลูป นั่นเป็นทางออกที่ดี
Tunji_D

2
นี่คือทองคำ สิ่งนี้นำฉันไปสู่โพสต์ของ Florina ซึ่งคุณควรอ่าน: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - ขอบคุณสำหรับคำใบ้ @ yeonseok.seo!
Benoit Duffez

1
@PRA เท่าที่ฉันรู้มันไม่สำคัญเลย docs.oracle.com/javase/specs/jls/se8/html/… Long จะถูก unboxed เป็น long และจะทำการทดสอบความเท่าเทียมกันของจำนวนเต็ม โปรดชี้ทางที่ถูกต้องให้ฉันถ้าฉันผิด
yeonseok.seo

83

สำหรับวิธีที่สวยงามยิ่งขึ้นฉันขอแนะนำสองตัวเลือก:

การตรวจสอบค่าส่งคืนจากinsertการดำเนินการด้วยIGNOREa OnConflictStrategy(ถ้ามันเท่ากับ -1 แสดงว่าไม่ได้แทรกแถว):

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

การจัดการข้อยกเว้นจากinsertการดำเนินการโดยFAILเป็น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);
    }
}

9
ใช้งานได้ดีสำหรับแต่ละเอนทิตี แต่ยากที่จะนำไปใช้กับคอลเล็กชัน เป็นการดีที่จะกรองคอลเล็กชันที่แทรกและกรองออกจากการอัปเดต
Tunji_D

2
@DanielWilson ขึ้นอยู่กับแอปพลิเคชันของคุณคำตอบนี้ใช้ได้ดีกับเอนทิตีเดียวอย่างไรก็ตามไม่สามารถใช้ได้กับรายการเอนทิตีที่เป็นสิ่งที่ฉันมี
Tunji_D

2
ไม่ว่าด้วยเหตุผลใดก็ตามเมื่อฉันทำแนวทางแรกการแทรก ID ที่มีอยู่แล้วจะส่งคืนหมายเลขแถวที่มากกว่าที่มีอยู่ไม่ใช่ -1L
ElliotM

1
ดังที่ Ohmnibus กล่าวในคำตอบอื่น ๆ ให้ทำเครื่องหมายupsertวิธีการด้วย@Transactionคำอธิบายประกอบที่ดีกว่า- stackoverflow.com/questions/45677230/…
Dr.jacky

คุณสามารถอธิบายได้ว่าทำไมคำอธิบายประกอบ @Update จึงมีกลยุทธ์ที่ขัดแย้งกันของ FAIL หรือ IGNORE Room จะพิจารณาว่าแบบสอบถามการอัปเดตมีข้อขัดแย้งในกรณีใดบ้าง หากฉันจะตีความกลยุทธ์ความขัดแย้งอย่างไร้เดียงสาในคำอธิบายประกอบการอัปเดตฉันจะบอกว่าเมื่อมีบางสิ่งที่ต้องอัปเดตมีข้อขัดแย้งและจะไม่มีวันอัปเดต แต่นั่นไม่ใช่พฤติกรรมที่ฉันเห็น อาจมีข้อขัดแย้งในการสอบถามการอัปเดตได้หรือไม่? หรือมีข้อขัดแย้งเกิดขึ้นหากการอัปเดตทำให้ข้อ จำกัด ของคีย์เฉพาะอื่นล้มเหลว
Hylke

41

ฉันไม่พบแบบสอบถาม SQLite ที่จะแทรกหรืออัปเดตโดยไม่ทำให้เกิดการเปลี่ยนแปลงที่ไม่ต้องการกับคีย์นอกของฉันดังนั้นฉันจึงเลือกที่จะแทรกก่อนโดยไม่สนใจข้อขัดแย้งหากเกิดขึ้นและอัปเดตทันทีหลังจากนั้นโดยไม่สนใจข้อขัดแย้งอีกครั้ง

วิธีการแทรกและการอัพเดตได้รับการป้องกันเพื่อให้คลาสภายนอกเห็นและใช้วิธีการอัพเท่านั้น โปรดทราบว่านี่ไม่ใช่การเพิ่มขึ้นอย่างแท้จริงราวกับว่า MyEntity POJOS ใด ๆ มีช่องว่างพวกเขาจะเขียนทับสิ่งที่อาจอยู่ในฐานข้อมูลในปัจจุบัน นี่ไม่ใช่ข้อแม้สำหรับฉัน แต่อาจเป็นสำหรับแอปพลิเคชันของคุณ

@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
คุณอาจต้องการทำให้มีประสิทธิภาพมากขึ้นและตรวจสอบค่าส่งคืน -1 ส่งสัญญาณความขัดแย้งไม่ว่าชนิดใดก็ตาม
jcuypers

22
ทำเครื่องหมายupsertวิธีการด้วย@Transactionคำอธิบายประกอบให้ดีขึ้น
Ohmnibus

3
ฉันเดาว่าวิธีที่เหมาะสมในการทำเช่นนี้คือการถามว่าค่านั้นอยู่ในฐานข้อมูลอยู่แล้วหรือไม่ (โดยใช้คีย์หลัก) คุณสามารถทำได้โดยใช้ abstractClass (เพื่อแทนที่อินเทอร์เฟซ dao) หรือใช้คลาสที่เรียกไปยัง dao ของวัตถุ
Sebastian Corradi

@Ohmnibus ไม่เนื่องจากเอกสารระบุว่า> การใส่คำอธิบายประกอบนี้ในวิธีการแทรกอัปเดตหรือลบไม่มีผลกระทบเนื่องจากมีการเรียกใช้ภายในธุรกรรมเสมอ ในทำนองเดียวกันหากมีการใส่คำอธิบายประกอบด้วย Query แต่รันคำสั่งอัพเดตหรือลบคำสั่งนั้นจะรวมอยู่ในธุรกรรมโดยอัตโนมัติ ดูเอกสารธุรกรรม
Levon Vardanyan

1
@LevonVardanyan ตัวอย่างในหน้าที่คุณเชื่อมโยงแสดงวิธีการที่คล้ายกับ upsert ซึ่งประกอบด้วยการแทรกและการลบ นอกจากนี้เราไม่ได้ใส่คำอธิบายประกอบลงในส่วนแทรกหรืออัปเดต แต่เป็นวิธีการที่มีทั้งสองอย่าง
Ohmnibus

8

หากตารางมีคอลัมน์มากกว่าหนึ่งคอลัมน์คุณสามารถใช้

@Insert(onConflict = OnConflictStrategy.REPLACE)

เพื่อแทนที่แถว

อ้างอิง - ไปที่เคล็ดลับ Android Room Codelab


21
กรุณาอย่าใช้วิธีนี้ หากคุณมีคีย์แปลกปลอมที่กำลังดูข้อมูลของคุณมันจะทริกเกอร์ onDelete listener และคุณอาจไม่ต้องการสิ่งนั้น
Alexandr Zhurkov

@AlexandrZhurkov ฉันคิดว่ามันควรจะเริ่มต้นเมื่อมีการอัปเดตเท่านั้นจากนั้นผู้ฟังใด ๆ ที่ใช้สิ่งนี้จะทำอย่างถูกต้อง อย่างไรก็ตามถ้าเรามีผู้ฟังข้อมูลและทริกเกอร์ onDelete ก็จะต้องจัดการด้วยรหัส
Vikas Pandey

@AlexandrZhurkov สิ่งนี้ทำงานได้ดีเมื่อตั้งค่าdeferred = trueในเอนทิตีด้วยคีย์ต่างประเทศ
ubuntudroid

@ubuntudroid มันทำงานได้ไม่ดีแม้ว่าจะตั้งค่าแฟล็กนั้นบนคีย์ต่างประเทศของเอนทิตีเพิ่งทดสอบ การโทรลบจะยังคงดำเนินต่อไปเมื่อการทำธุรกรรมเสร็จสมบูรณ์เนื่องจากไม่ได้ถูกปิดในระหว่างกระบวนการ แต่จะไม่เกิดขึ้นเมื่อเกิดขึ้น แต่เมื่อสิ้นสุดธุรกรรมยังคงอยู่
เงา

5

นี่คือรหัสใน 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) ควรเป็น val id = insert (entity) สำหรับ kotlin
Kibotu

@Sam วิธีจัดการกับnull valuesที่ที่ฉันไม่ต้องการอัปเดตด้วย null แต่ยังคงรักษาค่าเก่าไว้ เหรอ?
binrebin

3

เพียงอัปเดตวิธีดำเนินการกับ Kotlin ที่เก็บข้อมูลของโมเดล (อาจจะใช้ในตัวนับตามตัวอย่าง):

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

คุณยังสามารถใช้ @Transaction และตัวแปรตัวสร้างฐานข้อมูลสำหรับธุรกรรมที่ซับซ้อนมากขึ้นโดยใช้ database.openHelper.writableDatabase.execSQL ("SQL STATEMENT")


0

อีกวิธีหนึ่งที่ฉันคิดได้คือรับเอนทิตีผ่าน DAO โดยการสืบค้นจากนั้นดำเนินการอัปเดตที่ต้องการ สิ่งนี้อาจมีประสิทธิภาพน้อยกว่าเมื่อเทียบกับโซลูชันอื่น ๆ ในเธรดนี้ในแง่ของรันไทม์เนื่องจากต้องดึงเอนทิตีแบบเต็ม แต่ให้ความยืดหยุ่นมากขึ้นในแง่ของการดำเนินการที่อนุญาตเช่นฟิลด์ / ตัวแปรที่จะอัปเดต

ตัวอย่างเช่น :

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

0

ควรเป็นไปได้ด้วยคำสั่งประเภทนี้:

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

คุณหมายถึงอะไร? ON CONFLICT UPDATE SET a = 1, b = 2ไม่รองรับRoom @Queryคำอธิบายประกอบ
ขึ้น

-1

หากคุณมีรหัสเดิม: บางเอนทิตีใน Java และBaseDao as Interface(ที่คุณไม่สามารถเพิ่มเนื้อความของฟังก์ชันได้) หรือคุณขี้เกียจเกินไปที่จะแทนที่ทั้งหมดimplementsด้วยextendsJava-children

หมายเหตุ: ใช้งานได้เฉพาะในโค้ด Kotlin ฉันแน่ใจว่าคุณเขียนโค้ดใหม่ใน Kotlin ฉันพูดถูก? :)

ในที่สุดวิธีแก้ขี้เกียจคือการเพิ่มสอง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)
    }
}

ดูเหมือนว่าจะมีข้อบกพร่อง? ไม่ได้สร้างธุรกรรมอย่างถูกต้อง
Rawa
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.