Biblioteca Android Room Persistence: Upsert

97

A biblioteca de persistência Room do Android graciosamente inclui as anotações @Insert e @Update que funcionam para objetos ou coleções. No entanto, tenho um caso de uso (notificações push contendo um modelo) que exigiria um UPSERT, pois os dados podem ou não existir no banco de dados.

O Sqlite não tem upsert nativamente e as soluções alternativas são descritas nesta pergunta do SO . Dadas as soluções lá, como aplicá-las à Room?

Para ser mais específico, como posso implementar uma inserção ou atualização na Room que não quebraria nenhuma restrição de chave estrangeira? Usar insert com onConflict = REPLACE fará com que o onDelete de qualquer chave estrangeira para aquela linha seja chamada. No meu caso, onDelete causa uma cascata e a reinserção de uma linha fará com que as linhas em outras tabelas com a chave estrangeira sejam excluídas. Este NÃO é o comportamento pretendido.

Tunji_D
fonte

Respostas:

76

Talvez você possa fazer seu BaseDao assim.

proteja a operação de upsert com @Transaction e tente atualizar apenas se a inserção falhar.

@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
fonte
Isso seria ruim para o desempenho, pois haverá várias interações de banco de dados para cada elemento da lista.
Tunji_D
13
mas, NÃO há "inserir no loop for".
yeonseok.seo
4
você está absolutamente correto! Eu perdi isso, pensei que você estava inserindo no loop for. Essa é uma ótima solução.
Tunji_D
2
Isso é ouro. Isso me levou à postagem de Florina, que você deve ler: medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1 - obrigado pela dica @ yeonseok.seo!
Benoit Duffez 01 de
1
@PRA, pelo que eu sei, não importa nada. docs.oracle.com/javase/specs/jls/se8/html/… Long será desencaixotado em long e será realizado o teste de igualdade de inteiros. por favor, me indique a direção certa se eu estiver errado.
yeonseok.seo
78

Para uma maneira mais elegante de fazer isso, sugiro duas opções:

Verificando o valor de retorno da insertoperação com IGNOREas a OnConflictStrategy(se for igual a -1, significa que a linha não foi inserida):

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

Lidando com exceção de insertoperação FAILcomo 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);
    }
}
user3448282
fonte
9
isso funciona bem para entidades individuais, mas é difícil de implementar para uma coleção. Seria bom filtrar quais coleções foram inseridas e filtrá-las da atualização.
Tunji_D
2
@DanielWilson depende da sua aplicação, esta resposta funciona bem para entidades únicas, porém não se aplica para uma lista de entidades que é o que eu tenho.
Tunji_D
2
Por qualquer motivo, quando faço a primeira abordagem, inserir um ID já existente retorna um número de linha maior do que o existente, não -1L.
ElliotM
41

Não consegui encontrar uma consulta SQLite que pudesse inserir ou atualizar sem causar alterações indesejadas em minha chave estrangeira, então, em vez disso, optei por inserir primeiro, ignorando os conflitos se eles ocorressem e atualizando imediatamente depois, novamente ignorando os conflitos.

Os métodos de inserção e atualização são protegidos para que as classes externas vejam e usem apenas o método upsert. Lembre-se de que este não é um verdadeiro upsert, pois se algum dos POJOS do MyEntity tiver campos nulos, eles substituirão o que pode estar atualmente no banco de dados. Esta não é uma advertência para mim, mas pode ser para sua aplicação.

@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
fonte
6
você pode querer torná-lo mais eficiente e verificar os valores de retorno. -1 sinaliza conflito de qualquer tipo.
jcuypers de
21
É melhor marcar o upsertmétodo com a @Transactionanotação
Ohmnibus
3
Acho que a maneira correta de fazer isso é perguntando se o valor já estava no banco de dados (usando sua chave primária). você pode fazer isso usando uma abstractClass (para substituir a interface dao) ou usando a classe que chama a dao do objeto
Sebastian Corradi
@Ohmnibus não, porque a documentação diz> Colocar esta anotação em um método Insert, Update ou Delete não tem impacto porque eles sempre são executados dentro de uma transação. Da mesma forma, se ele estiver anotado com Query, mas executar uma instrução update ou delete, ele será automaticamente empacotado em uma transação. Ver documento de transação
Levon Vardanyan
1
@LevonVardanyan o exemplo na página que você vinculou mostra um método muito semelhante ao upsert, contendo uma inserção e uma exclusão. Além disso, não estamos colocando a anotação em uma inserção ou atualização, mas em um método que contém ambas.
Ohmnibus de
8

Se a tabela tiver mais de uma coluna, você pode usar

@Insert(onConflict = OnConflictStrategy.REPLACE)

para substituir uma linha.

Referência - Vá para dicas Android Room Codelab

Vikas Pandey
fonte
18
Não use este método. Se você tiver qualquer chave estrangeira examinando seus dados, ela será acionada em Excluir ouvinte e você provavelmente não quer isso
Alexandr Zhurkov
@AlexandrZhurkov, acho que deve ser acionado apenas na atualização, então qualquer ouvinte, se implementado, o faria corretamente. De qualquer forma, se tivermos um ouvinte nos dados e gatilhos onDelete, então ele deve ser tratado pelo código
Vikas Pandey
@AlexandrZhurkov Isso funciona bem ao definir deferred = truena entidade com a chave estrangeira.
ubuntudroid de
4

Este é o código em 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)
  }

}

Sam
fonte
1
long id = inserir (entidade) deve ser val id = inserir (entidade) para kotlin
Kibotu
@ Sam, como lidar com null valuesonde não quero atualizar com nulo, mas reter o valor antigo. ?
binrebin
3

Apenas uma atualização de como fazer isso com Kotlin retendo dados do modelo (talvez para usá-lo em um contador como no exemplo):

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

Você também pode usar @Transaction e variável de construtor de banco de dados para transações mais complexas usando database.openHelper.writableDatabase.execSQL ("SQL STATEMENT")

emirua
fonte
0

Outra abordagem que posso pensar é obter a entidade por meio do DAO por consulta e, em seguida, executar as atualizações desejadas. Isso pode ser menos eficiente em comparação com as outras soluções neste segmento em termos de tempo de execução por ter que recuperar a entidade completa, mas permite muito mais flexibilidade em termos de operações permitidas, como em quais campos / variáveis ​​atualizar.

Por exemplo :

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

Deve ser possível com este tipo de declaração:

INSERT INTO table_name (a, b) VALUES (1, 2) ON CONFLICT UPDATE SET a = 1, b = 2
Brill Pappin
fonte
O que você quer dizer? ON CONFLICT UPDATE SET a = 1, b = 2não é compatível com Room @Queryanotações.
ausente em