SQLite UPSERT / UPDATE OR INSERT

103

Eu preciso realizar UPSERT / INSERT OR UPDATE em um banco de dados SQLite.

Existe o comando INSERT OR REPLACE que em muitos casos pode ser útil. Mas se você quiser manter seus ids com incremento automático no lugar por causa de chaves estrangeiras, isso não funciona, pois exclui a linha, cria uma nova e, conseqüentemente, esta nova linha tem um novo ID.

Esta seria a mesa:

jogadores - (chave primária no id, user_name exclusivo)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |
bgusach
fonte

Respostas:

52

Esta é uma resposta tardia. A partir do SQLIte 3.24.0, lançado em 4 de junho de 2018, finalmente há um suporte para a cláusula UPSERT seguindo a sintaxe do PostgreSQL.

INSERT INTO players (user_name, age)
  VALUES('steven', 32) 
  ON CONFLICT(user_name) 
  DO UPDATE SET age=excluded.age;

Observação: para aqueles que precisam usar uma versão do SQLite anterior a 3.24.0, consulte esta resposta abaixo (postada por mim, @MarqueIV).

No entanto, se você tiver a opção de atualizar, é fortemente encorajado a fazê-lo, pois, ao contrário da minha solução, a que postamos aqui atinge o comportamento desejado em uma única instrução. Além disso, você obtém todos os outros recursos, melhorias e correções de bugs que geralmente vêm com uma versão mais recente.

prapin
fonte
Por enquanto, não esta versão no repositório do Ubuntu ainda.
bl79
Por que não posso usar isso no Android? Eu tentei db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Dá-me um erro de sintaxe na palavra "on"
Bastian Voigt
1
@BastianVoigt Porque as bibliotecas SQLite3 instaladas em várias versões do Android são anteriores a 3.24.0. Veja: developer.android.com/reference/android/database/sqlite/… Infelizmente, se você precisa de um novo recurso do SQLite3 (ou qualquer outra biblioteca do sistema) no Android ou iOS, você precisa agrupar uma versão específica do SQLite em seu aplicativo em vez de depender do sistema instalado.
prapin
Em vez de UPSERT, isso não é mais um INDATE, já que tenta a inserção primeiro? ;)
Mark A. Donohoe
@BastianVoigt, por favor veja minha resposta abaixo (link na pergunta acima) que é para versões anteriores a 3.24.0.
Mark A. Donohoe
106

Estilo de perguntas e respostas

Bem, depois de pesquisar e lutar com o problema por horas, descobri que existem duas maneiras de fazer isso, dependendo da estrutura da sua tabela e se você tem restrições de chaves estrangeiras ativadas para manter a integridade. Eu gostaria de compartilhar isso em um formato limpo para economizar algum tempo para as pessoas que podem estar na minha situação.


Opção 1: você pode excluir a linha

Em outras palavras, você não tem uma chave estrangeira ou, se as tiver, seu mecanismo SQLite está configurado para que não haja exceções de integridade. O caminho a seguir é INSERT OR REPLACE . Se você está tentando inserir / atualizar um jogador cujo ID já existe, o mecanismo SQLite irá deletar aquela linha e inserir os dados que você está fornecendo. Agora surge a pergunta: o que fazer para manter o antigo ID associado?

Digamos que queremos UPSERT com os dados user_name = 'steven' e idade = 32.

Olhe para este código:

INSERT INTO players (id, name, age)

VALUES (
    coalesce((select id from players where user_name='steven'),
             (select max(id) from drawings) + 1),
    32)

O truque está em coalescer. Ele retorna o id do usuário 'steven' se houver, e caso contrário, ele retorna um novo id novo.


Opção 2: você não pode excluir a linha

Depois de mexer na solução anterior, percebi que no meu caso isso poderia acabar destruindo dados, já que esse ID funciona como uma chave estrangeira para outra tabela. Além disso, criei a tabela com a cláusula ON DELETE CASCADE , o que significaria que ela apagaria os dados silenciosamente. Perigoso.

Então, primeiro pensei em uma cláusula IF, mas o SQLite só tem CASE . E este CASE não pode ser usado (ou pelo menos eu não consegui) para realizar uma consulta UPDATE se EXISTS (selecione id de jogadores onde user_name = 'steven'), e INSERT se não. Não vá.

E então, finalmente, usei a força bruta, com sucesso. A lógica é, para cada UPSERT que você deseja realizar, primeiro execute um INSERT OU IGNORE para ter certeza de que há uma linha com nosso usuário e, em seguida, execute uma consulta UPDATE com exatamente os mesmos dados que você tentou inserir.

Os mesmos dados de antes: user_name = 'steven' e idade = 32.

-- make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

-- make sure it has the right data
UPDATE players SET user_name='steven', age=32 WHERE user_name='steven'; 

E isso é tudo!

EDITAR

Como Andy comentou, tentar inserir primeiro e depois atualizar pode levar ao disparo de gatilhos com mais frequência do que o esperado. Isso não é, em minha opinião, um problema de segurança de dados, mas é verdade que disparar eventos desnecessários faz pouco sentido. Portanto, uma solução melhorada seria:

-- Try to update any existing row
UPDATE players SET age=32 WHERE user_name='steven';

-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 
bgusach
fonte
10
Idem ... a opção 2 é ótima. Exceto que eu fiz ao contrário: tente uma atualização, verifique se rowsAffected> 0, senão faça uma inserção.
Tom Spencer
Essa é uma abordagem muito boa também, a única pequena desvantagem é que você não tem apenas um SQL para o "upsert".
bgusach
2
você não precisa redefinir user_name na instrução update no último exemplo de código. É o suficiente para definir a idade.
Serg Stetsuk
72

Aqui está uma abordagem que não requer a força bruta 'ignorar', que só funcionaria se houvesse uma violação de chave. Desta forma, funciona com base em qualquer condições que você especificar na atualização.

Experimente isso ...

-- Try to update any existing row
UPDATE players
SET age=32
WHERE user_name='steven';

-- If no update happened (i.e. the row didn't exist) then insert one
INSERT INTO players (user_name, age)
SELECT 'steven', 32
WHERE (Select Changes() = 0);

Como funciona

O 'molho mágico' aqui está usando Changes()na Wherecláusula. Changes()representa o número de linhas afetadas pela última operação, que neste caso é a atualização.

No exemplo acima, se não houver mudanças desde a atualização (ou seja, o registro não existe), então Changes()= 0, então a Wherecláusula na Insertinstrução é avaliada como verdadeira e uma nova linha é inserida com os dados especificados.

Se o Update fez atualização uma linha existente, então Changes()= 1 (ou mais precisamente, não zero se mais do que uma linha foi atualizado), de modo que o 'onde' cláusula noInsert agora avaliada como falsa e, portanto, nenhuma inserção terá lugar.

A beleza disso é que não há necessidade de força bruta, nem exclusão desnecessária e, em seguida, reinserção de dados, o que pode resultar na bagunça de chaves downstream em relacionamentos de chave estrangeira.

Além disso, como é apenas uma Wherecláusula padrão , pode se basear em qualquer coisa que você definir, não apenas em violações de chave. Da mesma forma, você pode usar Changes()em combinação com qualquer coisa que quiser / precisar em qualquer lugar que as expressões sejam permitidas.

Mark A. Donohoe
fonte
1
Isso funcionou muito bem para mim. Eu não vi essa solução em nenhum outro lugar junto com todos os exemplos INSERT OR REPLACE, é muito mais flexível para o meu caso de uso.
csab
@MarqueIV e se houver dois itens que devem ser atualizados ou inseridos? por exemplo, o primeiro foi atualizado e o segundo não existe. nesse caso Changes() = 0, retornará falso e duas linhas farão INSERT OU REPLACE
Andriy Antonov
Normalmente, um UPSERT deve atuar em um registro. Se você está dizendo que tem certeza de que está agindo em mais de um registro, altere a verificação de contagem de acordo.
Mark A. Donohoe
O ruim é que, se a linha existe, o método de atualização deve ser executado independentemente de a linha ter sido alterada ou não.
Jimi
1
Por que isso é ruim? E se os dados não mudaram, por que você está ligando UPSERTem primeiro lugar? Mas mesmo assim, é uma boa coisa que a atualização aconteça, configurando Changes=1ou então a INSERTinstrução seria disparada incorretamente, o que você não quer.
Mark A. Donohoe
25

O problema com todas as respostas apresentadas é a completa falta de levar os gatilhos (e provavelmente outros efeitos colaterais) em consideração. Solução como

INSERT OR IGNORE ...
UPDATE ...

leva a ambos os gatilhos executados (para inserir e depois para atualizar) quando a linha não existe.

A solução adequada é

UPDATE OR IGNORE ...
INSERT OR IGNORE ...

nesse caso, apenas uma instrução é executada (quando a linha existe ou não).

Andy
fonte
1
Eu vejo seu ponto. Vou atualizar minha pergunta. A propósito, não sei por que UPDATE OR IGNOREé necessário, já que a atualização não irá travar se nenhuma linha for encontrada.
bgusach
1
legibilidade? Eu posso ver o que o código de Andy está fazendo rapidamente. Seu bgusach eu tive que estudar um minuto para descobrir.
Brandan
6

Para ter um UPSERT puro sem orifícios (para programadores) que não retransmitem em chaves únicas e outras:

UPDATE players SET user_name="gil", age=32 WHERE user_name='george'; 
SELECT changes();

SELECT changes () retornará o número de atualizações feitas na última consulta. Em seguida, verifique se o valor de retorno de changes () é 0, se for o caso, execute:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 
Gilco
fonte
Isso é equivalente ao que @fiznool propôs em seu comentário (embora eu optasse por sua solução). Está tudo bem e realmente funciona bem, mas você não tem uma instrução SQL exclusiva. O UPSERT não baseado em PK ou outras chaves exclusivas faz pouco ou nenhum sentido para mim.
bgusach 01 de
4

Você também pode simplesmente adicionar uma cláusula ON CONFLICT REPLACE à sua restrição única user_name e então apenas inserir INSERT, deixando para SQLite descobrir o que fazer em caso de conflito. Veja: https://sqlite.org/lang_conflict.html .

Observe também a frase sobre os gatilhos de exclusão: Quando a estratégia de resolução de conflito REPLACE exclui linhas para satisfazer uma restrição, os gatilhos de exclusão são acionados se e somente se os gatilhos recursivos estiverem habilitados.

Maximilian Tyrtania
fonte
1

Opção 1: Inserir -> Atualizar

Se você gosta de evitar ambos changes()=0eINSERT OR IGNORE mesmo se não puder excluir a linha - você pode usar essa lógica;

Primeiro, insira (se não existir) e depois atualize filtrando com a chave exclusiva.

Exemplo

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Insert if NOT exists
INSERT INTO players (user_name, age)
SELECT 'johnny', 20
WHERE NOT EXISTS (SELECT 1 FROM players WHERE user_name='johnny' AND age=20);

-- Update (will affect row, only if found)
-- no point to update user_name to 'johnny' since it's unique, and we filter by it as well
UPDATE players 
SET age=20 
WHERE user_name='johnny';

Em relação aos gatilhos

Aviso: não testei para ver quais gatilhos estão sendo chamados, mas presumo o seguinte:

se a linha não existe

  • ANTES DE INSERIR
  • INSERT usando INSTEAD OF
  • APÓS A INSERÇÃO
  • ANTES DA ATUALIZAÇÃO
  • ATUALIZAR usando INSTEAD OF
  • APÓS ATUALIZAÇÃO

se existe linha

  • ANTES DA ATUALIZAÇÃO
  • ATUALIZAR usando INSTEAD OF
  • APÓS ATUALIZAÇÃO

Opção 2: inserir ou substituir - manter seu próprio ID

desta forma, você pode ter um único comando SQL

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Single command to insert or update
INSERT OR REPLACE INTO players 
(id, user_name, age) 
VALUES ((SELECT id from players WHERE user_name='johnny' AND age=20),
        'johnny',
        20);

Editar: opção 2 adicionada.

itsho
fonte