Thư viện độ bền phòng Android: Nâng cấp


97

Thư viện bền bỉ trong Room của Android bao gồm các chú thích @Insert và @Update hoạt động cho các đối tượng hoặc bộ sưu tập. Tuy nhiên, tôi có một trường hợp sử dụng (thông báo đẩy có chứa một mô hình) sẽ yêu cầu UPSERT vì dữ liệu có thể tồn tại hoặc có thể không tồn tại trong cơ sở dữ liệu.

Sqlite không có upert nguyên bản và các cách giải quyết được mô tả trong câu hỏi SO này . Với các giải pháp ở đó, người ta sẽ áp dụng chúng vào Room như thế nào?

Để cụ thể hơn, làm cách nào tôi có thể triển khai chèn hoặc cập nhật trong Room mà không phá vỡ bất kỳ ràng buộc khóa ngoại nào? Sử dụng insert với onConflict = REPLACE sẽ khiến onDelete cho bất kỳ khóa ngoại nào của hàng đó được gọi. Trong trường hợp của tôi, onDelete gây ra một dòng chảy và việc nhập lại một hàng sẽ khiến các hàng trong các bảng khác có khóa ngoại bị xóa. Đây KHÔNG phải là hành vi dự định.

Câu trả lời:


76

Có lẽ bạn có thể làm cho BaseDao của bạn như thế này.

đảm bảo hoạt động nâng cấp bằng @Transaction và chỉ cố gắng cập nhật nếu việc chèn không thành công.

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

Điều này sẽ không tốt cho hiệu suất vì sẽ có nhiều tương tác với cơ sở dữ liệu cho mọi phần tử trong danh sách.
Tunji_D

13
nhưng, KHÔNG có "chèn trong vòng lặp for".
yeonseok.seo

4
bạn hoàn toàn đúng! Tôi đã bỏ lỡ điều đó, tôi nghĩ rằng bạn đang chèn vào vòng lặp for. Đó là một giải pháp tuyệt vời.
Tunji_D

2
Đây là vàng. Điều này đã dẫn tôi đến bài đăng của Florina mà bạn nên đọc: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - cảm ơn vì gợi ý @ yeonseok.seo!
Benoit Duffez

1
@PRA theo như tôi biết, nó không có vấn đề gì cả. docs.oracle.com/javase/specs/jls/se8/html/… Long sẽ được mở hộp thành long và kiểm tra bình đẳng số nguyên sẽ được thực hiện. xin vui lòng chỉ cho tôi hướng đúng nếu tôi sai.
yeonseok.seo

78

Để có cách làm đẹp hơn, tôi sẽ đề xuất hai lựa chọn:

Kiểm tra giá trị trả về từ insertthao tác với IGNOREas a OnConflictStrategy(nếu nó bằng -1 thì có nghĩa là hàng không được chèn):

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

Xử lý ngoại lệ từ inserthoạt động với FAILnhư một 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
điều này hoạt động tốt cho các thực thể riêng lẻ, nhưng khó triển khai cho một bộ sưu tập. Sẽ rất tốt nếu bạn lọc những bộ sưu tập đã được chèn và lọc chúng ra khỏi bản cập nhật.
Tunji_D

2
@DanielWilson nó phụ thuộc vào ứng dụng của bạn, câu trả lời này hoạt động tốt cho các thực thể đơn lẻ, tuy nhiên, nó không áp dụng cho danh sách các thực thể mà tôi có.
Tunji_D

2
Vì bất kỳ lý do gì, khi tôi thực hiện cách tiếp cận đầu tiên, việc chèn một ID đã tồn tại sẽ trả về một số hàng lớn hơn những gì tồn tại, không phải -1L.
ElliotM

41

Tôi không thể tìm thấy truy vấn SQLite sẽ chèn hoặc cập nhật mà không gây ra các thay đổi không mong muốn đối với khóa ngoại của mình, vì vậy thay vào đó, tôi chọn chèn trước, bỏ qua xung đột nếu chúng xảy ra và cập nhật ngay sau đó, lại bỏ qua xung đột.

Các phương thức chèn và cập nhật được bảo vệ để các lớp bên ngoài chỉ nhìn thấy và sử dụng phương thức upert. Hãy nhớ rằng đây không phải là một nâng cấp thực sự vì nếu bất kỳ MyEntity POJOS nào có trường rỗng, chúng sẽ ghi đè lên những gì hiện có thể có trong cơ sở dữ liệu. Đây không phải là một cảnh báo trước đối với tôi, nhưng nó có thể dành cho ứng dụng của bạn.

@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
bạn có thể muốn làm cho nó hiệu quả hơn và kiểm tra các giá trị trả về. -1 tín hiệu xung đột dưới bất kỳ hình thức nào.
jcuypers

21
Đánh dấu upsertphương pháp tốt hơn với @Transactionchú thích
Ohmnibus

3
Tôi đoán cách thích hợp để làm điều này là hỏi xem giá trị đã có trên DB (sử dụng khóa chính của nó) hay chưa. bạn có thể làm điều đó bằng cách sử dụng abstractClass (để thay thế cho giao diện dao) hoặc sử dụng lớp mà cuộc gọi đến dao của đối tượng
Sebastian Corradi

@Ohmnibus không, bởi vì tài liệu cho biết> Đặt chú thích này vào một phương pháp Chèn, Cập nhật hoặc Xóa không có tác động vì chúng luôn được chạy bên trong một giao dịch. Tương tự, nếu nó được chú thích bằng Query nhưng chạy câu lệnh cập nhật hoặc xóa, nó sẽ tự động được bao bọc trong một giao dịch. Xem tài liệu giao dịch
Levon Vardanyan

1
@LevonVardanyan, ví dụ trong trang bạn đã liên kết hiển thị một phương pháp rất giống với upert, chứa một phần chèn và một phần xóa. Ngoài ra, chúng tôi không đặt chú thích vào một phần chèn hoặc bản cập nhật, mà là một phương thức chứa cả hai.
Ohmnibus

8

Nếu bảng có nhiều cột, bạn có thể sử dụng

@Insert(onConflict = OnConflictStrategy.REPLACE)

để thay thế một hàng.

Tham khảo - Đi tới mẹo Android Room Codelab


18
Vui lòng không sử dụng phương pháp này. Nếu bạn có bất kỳ khóa ngoại nào đang xem dữ liệu của mình, nó sẽ kích hoạt trình nghe onDelete và có thể bạn không muốn điều đó
Alexandr Zhurkov 14/02/19

@AlexandrZhurkov, tôi đoán nó sẽ chỉ kích hoạt khi cập nhật, sau đó bất kỳ người nghe nào nếu được triển khai điều này sẽ thực hiện đúng. Dù sao nếu chúng ta có trình xử lý dữ liệu và trình kích hoạt onDelete thì nó phải được xử lý bằng mã
Vikas Pandey

@AlexandrZhurkov Điều này hoạt động tốt khi thiết lập deferred = truetrên thực thể có khóa ngoại.
ubuntudroid

4

Đây là mã trong 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) nên là val id = insert (entity) cho kotlin
Kibotu

@Sam, làm thế nào để giải quyết null valuestrường hợp tôi không muốn cập nhật bằng null nhưng giữ lại giá trị cũ. ?
binrebin

3

Chỉ là bản cập nhật cho cách thực hiện việc này với Kotlin giữ lại dữ liệu của mô hình (Có thể sử dụng nó trong một bộ đếm như trong ví dụ):

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

Bạn cũng có thể sử dụng @Transaction và biến hàm tạo cơ sở dữ liệu cho các giao dịch phức tạp hơn bằng cách sử dụng database.openHelper.wualityDatabase.execSQL ("SQL STATEMENT")


0

Một cách tiếp cận khác mà tôi có thể nghĩ đến là lấy thực thể qua DAO bằng cách truy vấn, sau đó thực hiện bất kỳ cập nhật nào mong muốn. Điều này có thể kém hiệu quả hơn so với các giải pháp khác trong chuỗi này về thời gian chạy vì phải truy xuất toàn bộ thực thể, nhưng cho phép linh hoạt hơn nhiều về các hoạt động được phép như cập nhật trường / biến nào.

Ví dụ :

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

0

Sẽ có thể với loại tuyên bố này:

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

Ý anh là gì? ON CONFLICT UPDATE SET a = 1, b = 2không được hỗ trợ bởi Room @Querychú thích.
isabsent
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.