Android Room Persistence Library:Upsert


97

AndroidのRoom永続化ライブラリには、オブジェクトまたはコレクションに対して機能する@Insertおよび@Updateアノテーションが含まれています。ただし、データがデータベースに存在する場合と存在しない場合があるため、UPSERTを必要とするユースケース(モデルを含むプッシュ通知)があります。

Sqliteにはネイティブのアップサートはありません。回避策はこのSO質問で説明されています。そこでの解決策を考えると、どのようにそれらを部屋に適用するのでしょうか?

より具体的には、外部キー制約を壊さないようにRoomに挿入または更新を実装するにはどうすればよいですか?onConflict = REPLACEでinsertを使用すると、その行への外部キーのonDeleteが呼び出されます。私の場合、onDeleteはカスケードを引き起こし、行を再挿入すると、外部キーを持つ他のテーブルの行が削除されます。これは意図された動作ではありません。

回答:


76

おそらく、あなたはあなたの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
ただし、「forループへの挿入」はありません。
yeonseok.seo

4
あなたは絶対に正しいです!私はそれを逃した、あなたはforループに挿入していると思った。それは素晴らしい解決策です。
Tunji_D

2
これは金です。これは私をフロリーナの投稿に導きました、あなたは読むべきです: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になり、整数等価テストが実行されます。私が間違っている場合は、正しい方向を教えてください。
yeonseok.seo

78

よりエレガントな方法で行うには、2つのオプションを提案します。

asとしてのinsert操作からの戻り値をチェックします(-1の場合、行が挿入されなかったことを意味します):IGNOREOnConflictStrategy

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

asを使用したinsert操作からの例外の処理:FAILOnConflictStrategy

@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 2018年

2
なんらかの理由で、最初の方法を実行する場合、既存のIDを挿入すると、-1Lではなく、存在するものより大きい行番号が返されます。
ElliotM

41

外部キーに不要な変更を引き起こさずに挿入または更新するSQLiteクエリを見つけることができなかったため、代わりに、最初に挿入を選択し、競合が発生した場合は無視し、直後に更新し、再度競合を無視しました。

挿入および更新メソッドは保護されているため、外部クラスはupsertメソッドのみを参照および使用します。MyEntity POJOSのいずれかにnullフィールドがある場合、これは実際のアップサートではないことに注意してください。これらは現在データベースにあるものを上書きします。これは私にとっては警告ではありませんが、あなたのアプリケーションのためかもしれません。

@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 2017年

21
より良いマークupsertを持つメソッド@Transactionのアノテーション
Ohmnibus

3
これを行う適切な方法は、値が(主キーを使用して)すでにDBにあるかどうかを確認することです。これは、abstractClassを使用して(daoインターフェースを置き換えるために)、またはオブジェクトのdaoを呼び出すクラスを使用して行うことができます
Sebastian Corradi

@Ohmnibus no、ドキュメントによると>このアノテーションをInsert、Update、またはDeleteメソッドに配置しても、常にトランザクション内で実行されるため、影響はありません。同様に、クエリで注釈が付けられているが、更新または削除ステートメントを実行する場合は、自動的にトランザクションにラップされます。 トランザクションドキュメントを参照
Levon Vardanyan

1
@LevonVardanyanリンクしたページの例は、挿入と削除を含む、upsertと非常によく似たメソッドを示しています。また、アノテーションを挿入または更新ではなく、両方を含むメソッドに配置しています。
オームニバス2019

8

テーブルに複数の列がある場合は、

@Insert(onConflict = OnConflictStrategy.REPLACE)

行を置き換える。

リファレンス- ヒントに移動するAndroid Room Codelab


18
この方法は使用しないでください。データを参照する外部キーがある場合、それはonDeleteリスナーをトリガーし、おそらくそれを望まないでしょう
Alexandr Zhurkov

@AlexandrZhurkov、更新時にのみトリガーする必要があると思います。実装されているリスナーがあれば、正しく実行されます。とにかく、データとonDeleteトリガーのリスナーがある場合は、コードで処理する必要があります
Vikas Pandey

@AlexandrZhurkovこれdeferred = trueは、外部キーでエンティティを設定するときにうまく機能します。
ubuntudroid

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
長いID =インサート(エンティティ)はkotlin用バルID =インサート(エンティティ)であるべきである
Kibotu

@ Sam、null valuesnullで更新したくないが古い値を保持したい場合の対処方法。?
ビンレビン

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

私が考えることができるもう1つのアプローチは、クエリによって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
19
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.