138

Android's Room persistence library graciously includes the @Insert and @Update annotations that work for objects or collections. I however have a use case (push notifications containing a model) that would require an UPSERT as the data may or may not exist in the database.

Sqlite doesn't have upsert natively, and workarounds are described in this SO question. Given the solutions there, how would one apply them to Room?

To be more specific, how can I implement an insert or update in Room that would not break any foreign key constraints? Using insert with onConflict=REPLACE will cause the onDelete for any foreign key to that row to be called. In my case onDelete causes a cascade, and reinserting a row will cause rows in other tables with the foreign key to be deleted. This is NOT the intended behavior.

Tunji_D
  • 3,677
  • 3
  • 27
  • 35

14 Answers14

118

EDIT:

as @Tunji_D mentioned, Room officially supports @Upsert from version 2.5.0-alpha03. (release note)

please check his answer for more details.

OLD ANSWER:

Perhaps you can make your BaseDao like this.

secure the upsert operation with @Transaction, and try to update only if insertion is failed.

@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
  • 2,696
  • 3
  • 13
  • 14
  • This would be bad for performance as there will be multiple database interactions for every element in the list. – Tunji_D Jun 07 '18 at 20:16
  • for each insert in the for loop, Room will create and execute multiple sql statements and transactions, each with their respective Java Object allocations and garbage collection overhead. This article goes into a bit more detail https://hackernoon.com/squeezing-performance-from-sqlite-insertions-with-room-d769512f8330 – Tunji_D Jun 08 '18 at 19:44
  • 15
    but, there is NO "insert in the for loop". – yeonseok.seo Jun 11 '18 at 02:13
  • 3
    This is gold. This led me to Florina's post, which you should read: https://medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 — thanks for the hint @yeonseok.seo! – Benoit Duffez Nov 01 '18 at 20:56
  • The problem is this is **NOT** SQLite `UPSERT` equivalent. For example, if you have a `unique` key in the table, and you are inserting some row with **0** primary key and existing unique key with `OnConflictStrategy.IGNORE` strategy, then the row will not be inserted since unique key duplication, as expected. The problem is **0** primary key is a valid value for `insert`, but the wrong value for `update`, and the row will **not** be updated. In raw SQLite, you can use `ON CONFLICT(word) DO UPDATE` to handle this case. – James Bond Dec 22 '21 at 19:52
95

For more elegant way to do that I would suggest two options:

Checking for return value from insert operation with IGNORE as a OnConflictStrategy (if it equals to -1 then it means row wasn't inserted):

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

Handling exception from insert operation with FAIL as a 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);
    }
}
user3448282
  • 2,629
  • 3
  • 25
  • 47
  • 12
    this works well for individual entities, but is hard to implement for a collection. It'd be nice to filter what collections were inserted and filter them out from the update. – Tunji_D Apr 24 '18 at 16:23
  • 4
    For whatever reason, when I do the first approach, inserting an already existing ID returns a row number greater than what exists, not -1L. – em_ Feb 05 '19 at 21:18
  • 1
    As Ohmnibus said on the other answer, better mark the `upsert` method with the `@Transaction` annotation – https://stackoverflow.com/questions/45677230/android-room-persistence-library-upsert#comment87446805_45684981 – Dr.jacky Nov 06 '20 at 15:11
  • 2
    Can you explain why the @Update annotation has a conflict strategy of FAIL or IGNORE? In what cases will Room consider an update query to have a conflict anyways? If I would naively interpret the conflict strategy on the Update annotation, i would say that when there is something to update there is a conflict, and thus it will never update. But thats not the behaviour im seeing. Can there even be conflicts on update queries? Or do conflicts arise if an update causes another unique key constrain to fail? – Hylke Nov 18 '20 at 16:52
  • OnConflictStrategy.FAIL is deprecated now in version 2.3.0. According to documentation "OnConflictStrategy.FAIL does not work as expected. The transaction is rolled back. Use OnConflictStrategy.ABORT instead". See https://developer.android.com/reference/androidx/room/OnConflictStrategy – zafar142003 Oct 11 '21 at 20:47
52

EDIT:

Starting in version 2.5.0-alpha03, Room now has support for an @Upsert annotation. An example of its use can be seen in this pull request in the "Now in Android" sample app.

OLD ANSWER:

I could not find a SQLite query that would insert or update without causing unwanted changes to my foreign key, so instead I opted to insert first, ignoring conflicts if they occurred, and updating immediately afterwards, again ignoring conflicts.

The insert and update methods are protected so external classes see and use the upsert method only. Keep in mind that this isn't a true upsert as if any of the MyEntity POJOS have null fields, they will overwrite what may currently be in the database. This is not a caveat for me, but it may be for your application.

@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
  • 3,677
  • 3
  • 27
  • 35
  • 6
    you might want to make it more efficient and check for return values. -1 signals conflict of whatever kind. – jcuypers Nov 10 '17 at 18:32
  • 23
    Better mark the `upsert` method with the `@Transaction` annotation – Ohmnibus May 07 '18 at 13:11
  • 3
    I guess the proper way to do this is asking if the value was already on the DB (using its primary key). you can do that using a abstractClass (to replace the dao interface) or using the class that call to the object's dao – Sebastian Corradi Jul 12 '18 at 18:01
  • @Ohmnibus no ,because documentation says > Putting this annotation on an Insert, Update or Delete method has no impact because they are always run inside a transaction. Similarly, if it is annotated with Query but runs an update or delete statement, it is automatically wrapped in a transaction. [See Transaction doc](https://developer.android.com/reference/android/arch/persistence/room/Transaction#summary) – Levon Vardanyan Apr 04 '19 at 19:30
  • 2
    @LevonVardanyan the example in the page you linked show a method very similar to upsert, containing an insert and a delete. Also, we're not putting the annotation to an insert or update, but to a method that contains both. – Ohmnibus Apr 06 '19 at 12:16
  • it's in 2.5.0-beta01 now. Does the @Upsert work stable? I wonder why it's take so long to include Upsert function into Room. I think I will keep the manual implementation and wait for official release. – neo Oct 26 '22 at 03:40
  • As of `2.5.0-rc01`, the fallback from insert to update works only when there is a primary key conflict. If there is a uniqueness conflict on an index, an exception is thrown. Seems like a bug ? – Louis Jan 05 '23 at 16:50
11

If the table has more than one column, you can use

@Insert(onConflict = OnConflictStrategy.REPLACE)

to replace a row.

Reference - Go to tips Android Room Codelab

Vasily Kabunov
  • 6,511
  • 13
  • 49
  • 53
Vikas Pandey
  • 1,115
  • 1
  • 15
  • 25
  • 33
    Please don't use this method. If you have any foreign keys looking at your data, it will trigger onDelete listener and you probably don't want that – Alexandr Zhurkov Feb 14 '19 at 08:31
  • @AlexandrZhurkov, I guess it should trigger on update only, then any listener if implemented this would do it correctly. Anyway if we have listener on the data and onDelete triggers then it must be handled by code – Vikas Pandey Feb 19 '19 at 08:41
6

This is the code in 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)
  }
}
Boken
  • 4,825
  • 10
  • 32
  • 42
Sam
  • 446
  • 7
  • 17
3

Just an update for how to do this with Kotlin retaining data of the model (Maybe to use it in a counter as in example):

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

You can also use @Transaction and database constructor variable for more complex transactions using database.openHelper.writableDatabase.execSQL("SQL STATEMENT")

emirua
  • 498
  • 5
  • 14
3

I found an interesting reading about it here.

It is the "same" as posted on https://stackoverflow.com/a/50736568/4744263. But, if you want an idiomatic and clean Kotlin version, here you go:

    @Transaction
    open fun insertOrUpdate(objList: List<T>) = insert(objList)
        .withIndex()
        .filter { it.value == -1L }
        .forEach { update(objList[it.index]) }

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(obj: List<T>): List<Long>

    @Update
    abstract fun update(obj: T)
Filipe Brito
  • 5,329
  • 5
  • 32
  • 42
2

Alternatively to make UPSERT manually in loop like it's suggested in @yeonseok.seo post, we may use UPSERT feature provided by Sqlite v.3.24.0 in Android Room.

Nowadays, this feature is supported by Android 11 and 12 with default Sqlite version 3.28.0 and 3.32.2 respectively. If you need it in versions prior Android 11 you can replace default Sqlite with custom Sqlite project like this https://github.com/requery/sqlite-android (or built your own) to have this and other features that are available in latest Sqlite versions, but not available in Android Sqlite provided by default.

If you have Sqlite version starting from 3.24.0 on device, you can use UPSERT in Android Room like this:

@Query("INSERT INTO Person (name, phone) VALUES (:name, :phone) ON CONFLICT (name) DO UPDATE SET phone=excluded.phone")
fun upsert(name: String, phone: String)
devger
  • 703
  • 4
  • 12
  • 26
  • The only one answer with **real** upsert... I have a feeling that other posters just don't understand that the main feature of `upsert` is the ability to update a row when you don't know its ID. With `upsert` DB can automatically update the row using unique constraint only, without a primary key, without additional requests. – James Bond Dec 26 '21 at 17:28
  • Yes, this is real UPSERT from Sqlite. But you can see it's supported only in Android 11 and 12, but in prior versions it's not supported. Now, Android Room still doesn't support annotations for UPSERT feature in Android 11 and 12 even though Sqlite on devices with this versions support it. So, we have only `@Query("")` option to call real UPSERT feature on Android 11 and 12. Also, the most of answers here are posted at the time when there were no Android 11 and 12, so Sqlite versions on devices were not supporting UPSERT, that's why people had to use some workarounds. – devger Dec 28 '21 at 13:51
0

Another approach I can think of is to get the entity via DAO by query, and then perform any desired updates. This may be less efficient compared to the other solutions in this thread in terms of runtime because of having to retrieve the full entity, but allows much more flexibility in terms of operations allowed such as on what fields/variable to update.

For example :

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

Here is a way to use a real UPSERT clause in Room library.

The main advantage of this method is that you can update rows for which you don't know their ID.

  1. Setup Android SQLite support library in your project to use modern SQLite features on all devices:
  2. Inherit your daos from BasicDao.
  3. Probably, you want to add in your BasicEntity: abstract fun toMap(): Map<String, Any?>

Use UPSERT in your Dao:

@Transaction
private suspend fun upsert(entity: SomeEntity): Map<String, Any?> {
    return upsert(
        SomeEntity.TABLE_NAME,
        entity.toMap(),
        setOf(SomeEntity.SOME_UNIQUE_KEY),
        setOf(SomeEntity.ID),
    )
}
// An entity has been created. You will get ID.
val rawEntity = someDao.upsert(SomeEntity(0, "name", "key-1"))

// An entity has been updated. You will get ID too, despite you didn't know it before, just by unique constraint!
val rawEntity = someDao.upsert(SomeEntity(0, "new name", "key-1"))

BasicDao:

import android.database.Cursor
import androidx.room.*
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery

abstract class BasicDao(open val database: RoomDatabase) {
    /**
     * Upsert all fields of the entity except those specified in [onConflict] and [excludedColumns].
     *
     * Usually, you don't want to update PK, you can exclude it in [excludedColumns].
     *
     * [UPSERT](https://www.sqlite.org/lang_UPSERT.html) syntax supported since version 3.24.0 (2018-06-04).
     * [RETURNING](https://www.sqlite.org/lang_returning.html) syntax supported since version 3.35.0 (2021-03-12).
     */
    protected suspend fun upsert(
        table: String,
        entity: Map<String, Any?>,
        onConflict: Set<String>,
        excludedColumns: Set<String> = setOf(),
        returning: Set<String> = setOf("*")
    ): Map<String, Any?> {
        val updatableColumns = entity.keys
            .filter { it !in onConflict && it !in excludedColumns }
            .map { "`${it}`=excluded.`${it}`" }

        // build sql
        val comma = ", "
        val placeholders = entity.map { "?" }.joinToString(comma)
        val returnings = returning.joinToString(comma) { if (it == "*") it else "`${it}`" }
        val sql = "INSERT INTO `${table}` VALUES (${placeholders})" +
                " ON CONFLICT(${onConflict.joinToString(comma)}) DO UPDATE SET" +
                " ${updatableColumns.joinToString(comma)}" +
                " RETURNING $returnings"

        val query: SupportSQLiteQuery = SimpleSQLiteQuery(sql, entity.values.toTypedArray())
        val cursor: Cursor = database.openHelper.writableDatabase.query(query)

        return getCursorResult(cursor).first()
    }

    protected fun getCursorResult(cursor: Cursor, isClose: Boolean = true): List<Map<String, Any?>> {
        val result = mutableListOf<Map<String, Any?>>()
        while (cursor.moveToNext()) {
            result.add(cursor.columnNames.mapIndexed { index, columnName ->
                val columnValue = if (cursor.isNull(index)) null else cursor.getString(index)
                columnName to columnValue
            }.toMap())
        }

        if (isClose) {
            cursor.close()
        }
        return result
    }
}

Entity example:

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey

@Entity(
    tableName = SomeEntity.TABLE_NAME,
    indices = [Index(value = [SomeEntity.SOME_UNIQUE_KEY], unique = true)]
)
data class SomeEntity(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = ID)
    val id: Long,

    @ColumnInfo(name = NAME)
    val name: String,

    @ColumnInfo(name = SOME_UNIQUE_KEY)
    val someUniqueKey: String,
) {
    companion object {
        const val TABLE_NAME = "some_table"
        const val ID = "id"
        const val NAME = "name"
        const val SOME_UNIQUE_KEY = "some_unique_key"
    }

    fun toMap(): Map<String, Any?> {
        return mapOf(
            ID to if (id == 0L) null else id,
            NAME to name,
            SOME_UNIQUE_KEY to someUniqueKey
        )
    }
}
James Bond
  • 2,229
  • 1
  • 15
  • 26
0

@Upsert is now available in room Version 2.5.0-beta01 check out the release notes

Razin Tailor
  • 13
  • 1
  • 4
0
  • Article Link
  • Single line answer : When we insert with a “replace” conflict strategy, the old row is actually deleted and a new row is added. When we upsert, we are actually updating the existing row.
-1

Should be possible with this sort of statement:

INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2
Brill Pappin
  • 4,692
  • 1
  • 36
  • 36
  • What do you mean? `ON CONFLICT UPDATE SET a = 1, b = 2` is not supported by `Room` `@Query`annotation. – isabsent Dec 03 '19 at 15:17
-2

If you have legacy code: some entities in Java and BaseDao as Interface (where you cannot add a function body) or you too lazy for replacing all implements with extends for Java-children.

Note: It works only in Kotlin code. I'm sure that you write new code in Kotlin, I'm right? :)

Finally a lazy solution is to add two 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
  • 153
  • 1
  • 9