Android Room Persistence 라이브러리 : Upsert


102

Android의 Room 지속성 라이브러리에는 개체 또는 컬렉션에 대해 작동하는 @Insert 및 @Update 주석이 정중하게 포함되어 있습니다. 그러나 데이터가 데이터베이스에있을 수도 있고 없을 수도 있으므로 UPSERT가 필요한 사용 사례 (모델이 포함 된 푸시 알림)가 있습니다.

Sqlite에는 기본적으로 upsert가 없으며 해결 방법은이 SO 질문에 설명되어 있습니다. 거기에 솔루션이 주어지면 어떻게 Room에 적용할까요?

더 구체적으로 말하면 외래 키 제약 조건을 위반하지 않는 Room에서 삽입 또는 업데이트를 구현하려면 어떻게해야합니까? onConflict = REPLACE와 함께 insert를 사용하면 해당 행에 대한 모든 외래 키에 대한 onDelete가 호출됩니다. 제 경우에는 onDelete로 인해 계단식이 발생하고 행을 다시 삽입하면 외래 키가있는 다른 테이블의 행이 삭제됩니다. 이것은 의도 된 동작이 아닙니다.

답변:


80

아마도 BaseDao를 이렇게 만들 수 있습니다.

@Transaction으로 upsert 작업을 보호하고 삽입이 실패한 경우에만 업데이트를 시도하십시오.

@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
그러나 "for 루프에 삽입"은 없습니다.
yeonseok.seo

4
당신이 절대적으로 옳습니다! 나는 그것을 놓쳤다. 나는 당신이 for 루프에 삽입하고 있다고 생각했다. 그것은 훌륭한 솔루션입니다.
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은 long으로 unboxed되며 정수 동등성 테스트가 수행됩니다. 내가 틀렸다면 올바른 방향을 알려주십시오.
yeonseok.seo

80

더 우아한 방법으로 두 가지 옵션을 제안합니다.

insert작업 에서 반환 값을 확인 IGNORE합니다 OnConflictStrategy(-1과 같으면 행이 삽입되지 않았 음을 의미합니다).

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

에서 예외 처리 insert와 함께 작업을 FAILA와 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
이것은 개별 엔티티에 대해 잘 작동하지만 컬렉션에 대해서는 구현하기가 어렵습니다. 삽입 된 컬렉션을 필터링하고 업데이트에서 필터링하는 것이 좋습니다.
Tunji_D

2
@DanielWilson 응용 프로그램에 따라 다르지만이 답변은 단일 엔터티에 대해 잘 작동하지만 내가 가진 엔터티 목록에는 적용되지 않습니다.
Tunji_D

2
어떤 이유로 든 첫 번째 방법을 사용할 때 이미 존재하는 ID를 삽입하면 -1L이 아닌 기존 ID보다 큰 행 번호가 반환됩니다.
ElliotM

41

내 외래 키에 원치 않는 변경을 일으키지 않고 삽입하거나 업데이트 할 SQLite 쿼리를 찾을 수 없었기 때문에 대신 먼저 삽입하고 충돌이 발생하면 무시하고 즉시 업데이트하고 다시 충돌을 무시했습니다.

삽입 및 업데이트 메소드는 보호되므로 외부 클래스는 upsert 메소드 만보고 사용합니다. MyEntity POJOS에 널 필드가있는 것처럼 이것이 진정한 upsert가 아니라는 점을 명심하십시오. 현재 데이터베이스에있을 수있는 것을 덮어 씁니다. 이것은 저에게 경고가 아니지만 귀하의 응용 프로그램에 대한 것일 수 있습니다.

@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

21
더 나은 표시 upsert와 방법 @Transaction주석
Ohmnibus

3
이를 수행하는 적절한 방법은 값이 이미 DB에 있는지 묻는 것입니다 (기본 키 사용). 해당합니다 (DAO 인터페이스를 대체하는)을 abstractClass를 사용하거나 클래스를 사용하여이 작업을 수행 할 수있는 개체의 DAO를 호출
세바스찬 Corradi

@Ohmnibus no, 왜냐하면 문서에>이 주석을 삽입, 업데이트 또는 삭제 메소드에 두는 것은 항상 트랜잭션 내에서 실행되기 때문에 영향을 미치지 않습니다. 마찬가지로 Query로 주석을 달았지만 update 또는 delete 문을 실행하면 트랜잭션에서 자동으로 래핑됩니다. 거래 문서 참조
Levon Vardanyan

1
링크 한 페이지의 예에서 @LevonVardanyan은 삽입 및 삭제를 포함하는 upsert와 매우 유사한 메소드를 보여줍니다. 또한 주석을 삽입 또는 업데이트에 넣지 않고 둘 다 포함하는 메서드에 넣습니다.
Ohmnibus

8

테이블에 둘 이상의 열이있는 경우 다음을 사용할 수 있습니다.

@Insert(onConflict = OnConflictStrategy.REPLACE)

행을 대체합니다.

참조 -Android Room Codelab 팁으로 이동


19
이 방법을 사용하지 마십시오. 당신이 당신의 데이터를 찾고있는 외래 키가있는 경우, 그것은 onDelete 리스너를 트리거하고 당신은 아마 싶지 않아
알렉산드르 Zhurkov

@AlexandrZhurkov, 업데이트시에만 트리거되어야한다고 생각하며 구현 된 경우 모든 리스너가 올바르게 수행합니다. 우리는이 코드에 의해 처리해야하는 데이터와 onDelete 트리거에 대한 리스너가 어쨌든 경우
카스 펜디 교수

@AlexandrZhurkov deferred = true외래 키로 엔터티를 설정할 때 잘 작동합니다 .
ubuntudroid

@ubuntudroid 방금 테스트 한 엔티티 외래 키에 해당 플래그를 설정해도 제대로 작동하지 않습니다. 삭제 호출은 프로세스 중에 해제되지 않기 때문에 트랜잭션이 완료된 후에도 계속 진행되며, 발생할 때 발생하지 않고 트랜잭션의 끝에서 여전히 발생합니다.
Shadow

4

다음은 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 valuesnull로 업데이트하지 않고 오래된 값을 유지 하는 방법을 다루는 방법 . ?
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)
    }
}
}

database.openHelper.writableDatabase.execSQL ( "SQL STATEMENT")을 사용하여보다 복잡한 트랜잭션에 @Transaction 및 데이터베이스 생성자 변수를 사용할 수도 있습니다.


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.
isabsent

-1

일부 자바 엔티티와 : 당신은 레거시 코드가있는 경우 BaseDao as Interface(당신이 함수 본문을 추가 할 수 없습니다) 또는 모든 교체에 너무 게으른 implementsextends자바 어린이를.

참고 : 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.