SQLite UPSERT / UPDATE OU INSERT

103

Je dois effectuer UPSERT / INSERT OU UPDATE sur une base de données SQLite.

Il y a la commande INSÉRER OU REMPLACER qui dans de nombreux cas peut être utile. Mais si vous voulez garder vos identifiants avec auto-incrémentation en place à cause de clés étrangères, cela ne fonctionne pas car il supprime la ligne, en crée une nouvelle et par conséquent cette nouvelle ligne a un nouvel identifiant.

Ce serait la table:

joueurs - (clé primaire sur id, nom_utilisateur unique)

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

Réponses:

52

C'est une réponse tardive. À partir de SQLIte 3.24.0, publié le 4 juin 2018, il existe enfin un support de la clause UPSERT suivant la syntaxe PostgreSQL.

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

Remarque: pour ceux qui doivent utiliser une version de SQLite antérieure à 3.24.0, veuillez faire référence à cette réponse ci-dessous (publiée par moi, @MarqueIV).

Cependant, si vous avez la possibilité de mettre à niveau, vous êtes fortement encouragé à le faire car contrairement à ma solution, celle publiée ici réalise le comportement souhaité en une seule déclaration. De plus, vous bénéficiez de toutes les autres fonctionnalités, améliorations et corrections de bogues qui accompagnent généralement une version plus récente.

prapin
la source
Pour l'instant, pas encore cette version dans le référentiel Ubuntu.
bl79
Pourquoi ne puis-je pas utiliser ceci sur Android? J'ai essayé db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Me donne une erreur de syntaxe sur le mot "on"
Bastian Voigt
1
@BastianVoigt Parce que les bibliothèques SQLite3 installées sur différentes versions d'Android sont antérieures à 3.24.0. Voir: developer.android.com/reference/android/database/sqlite/... Malheureusement, si vous avez besoin d'une nouvelle fonctionnalité de SQLite3 (ou de toute autre bibliothèque système) sur Android ou iOS, vous devez regrouper une version spécifique de SQLite dans votre application au lieu de se fier au système installé.
prapin
Plutôt que UPSERT, n'est-ce pas plus un INDATE puisqu'il essaie d'abord l'insert? ;)
Mark A. Donohoe
@BastianVoigt, veuillez voir ma réponse ci-dessous (liée dans la question ci-dessus) qui concerne les versions antérieures à 3.24.0.
Mark A. Donohoe
106

Style de questions et réponses

Eh bien, après avoir recherché et combattu le problème pendant des heures, j'ai découvert qu'il y avait deux façons d'accomplir cela, selon la structure de votre table et si vous avez des restrictions de clés étrangères activées pour maintenir l'intégrité. Je voudrais partager ceci dans un format propre pour gagner du temps aux personnes qui peuvent être dans ma situation.


Option 1: vous pouvez vous permettre de supprimer la ligne

En d'autres termes, vous n'avez pas de clé étrangère, ou si vous en avez, votre moteur SQLite est configuré de sorte qu'il n'y ait pas d'exceptions d'intégrité. La voie à suivre est INSÉRER OU REMPLACER . Si vous essayez d'insérer / mettre à jour un lecteur dont l'ID existe déjà, le moteur SQLite supprimera cette ligne et insérera les données que vous fournissez. Maintenant, la question se pose: que faire pour conserver l'ancien ID associé?

Disons que nous voulons UPSERT avec les données user_name = 'steven' et age = 32.

Regardez ce code:

INSERT INTO players (id, name, age)

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

L'astuce est dans la fusion. Il renvoie l'identifiant de l'utilisateur «steven» le cas échéant, et sinon, il renvoie un nouvel identifiant frais.


Option 2: vous ne pouvez pas vous permettre de supprimer la ligne

Après avoir analysé la solution précédente, j'ai réalisé que dans mon cas, cela pourrait finir par détruire des données, car cet ID fonctionne comme une clé étrangère pour une autre table. D'ailleurs, j'ai créé le tableau avec la clause ON DELETE CASCADE , ce qui signifierait qu'elle supprimerait les données en silence. Dangereux.

Donc, j'ai d'abord pensé à une clause IF, mais SQLite n'a que CASE . Et ce CASE ne peut pas être utilisé (ou du moins je ne l'ai pas géré) pour effectuer une requête UPDATE si EXISTS (sélectionnez id parmi les joueurs où user_name = 'steven'), et INSERT si ce n'est pas le cas. Ne pas aller.

Et puis, finalement j'ai utilisé la force brute, avec succès. La logique est que, pour chaque UPSERT que vous souhaitez effectuer, exécutez d'abord un INSERT OU IGNORE pour vous assurer qu'il y a une ligne avec notre utilisateur, puis exécutez une requête UPDATE avec exactement les mêmes données que vous avez essayé d'insérer.

Mêmes données qu'auparavant: user_name = 'steven' et age = 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'; 

Et c'est tout!

ÉDITER

Comme Andy l'a commenté, essayer d'insérer d'abord, puis de mettre à jour peut conduire à déclencher des déclencheurs plus souvent que prévu. Ce n'est pas à mon avis un problème de sécurité des données, mais il est vrai que déclencher des événements inutiles n'a pas de sens. Par conséquent, une solution améliorée serait:

-- 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
la source
10
Idem ... l'option 2 est géniale. Sauf que je l'ai fait dans l'autre sens: essayez une mise à jour, vérifiez si rowsAffected> 0, sinon faites une insertion.
Tom Spencer
C'est aussi une très bonne approche, le seul petit inconvénient est que vous n'avez pas qu'un seul SQL pour le "upsert".
bgusach
2
vous n'avez pas besoin de réinitialiser user_name dans l'instruction de mise à jour dans le dernier exemple de code. Il suffit de fixer l'âge.
Serg Stetsuk
72

Voici une approche qui ne nécessite pas la force brute «ignorer» qui ne fonctionnerait qu'en cas de violation de clé. Cette méthode fonctionne en fonction de tout conditions que vous spécifiez dans la mise à jour.

Essaye ça...

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

Comment ça fonctionne

La «sauce magique» ici est utilisée Changes()dans la Whereclause. Changes()représente le nombre de lignes affectées par la dernière opération, qui dans ce cas est la mise à jour.

Dans l'exemple ci-dessus, s'il n'y a pas de changement depuis la mise à jour (c'est-à-dire que l'enregistrement n'existe pas), alors Changes()= 0 donc la Whereclause de l' Insertinstruction prend la valeur true et une nouvelle ligne est insérée avec les données spécifiées.

Si la Update a mis à jour une ligne existante, alors Changes()= 1 (ou plus précisément, pas zéro si plus d'une ligne a été mise à jour), donc la clause 'Where' dans leInsert now est évaluée à false et donc aucune insertion n'aura lieu.

La beauté de ceci est qu'il n'y a pas de force brute nécessaire, ni de suppression inutilement, puis de réinsertion de données, ce qui peut entraîner la confusion des clés en aval dans les relations de clé étrangère.

De plus, comme il ne s'agit que d'une Whereclause standard , elle peut être basée sur tout ce que vous définissez, pas seulement sur des violations de clé. De même, vous pouvez utiliser Changes()en combinaison avec tout ce que vous voulez / avez besoin partout où les expressions sont autorisées.

Mark A. Donohoe
la source
1
Cela a très bien fonctionné pour moi. Je n'ai vu cette solution nulle part ailleurs à côté de tous les exemples INSERT OR REPLACE, elle est beaucoup plus flexible pour mon cas d'utilisation.
csab
@MarqueIV et qu'en est-il s'il y a deux éléments qui doivent être mis à jour ou insérés? par exemple, le premier a été mis à jour et le second n'existe pas. dans ce cas Changes() = 0, retournera false et deux lignes feront INSERT OR REPLACE
Andriy Antonov
Habituellement, un UPSERT est censé agir sur un seul enregistrement. Si vous dites que vous savez avec certitude qu'il agit sur plus d'un enregistrement, modifiez le contrôle de comptage en conséquence.
Mark A. Donohoe du
La mauvaise chose est que si la ligne existe, la méthode de mise à jour doit être exécutée indépendamment du fait que la ligne ait changé ou non.
Jimi
1
Pourquoi est-ce une mauvaise chose? Et si les données n'ont pas changé, pourquoi appelez-vous UPSERTen premier lieu? Mais même ainsi, c'est une bonne chose que la mise à jour se produise, Changes=1sinon la INSERTdéclaration se déclencherait de manière incorrecte, ce que vous ne voulez pas.
Mark A. Donohoe
25

Le problème avec toutes les réponses présentées est l'absence totale de prise en compte des déclencheurs (et probablement d'autres effets secondaires). Solution comme

INSERT OR IGNORE ...
UPDATE ...

conduit à l'exécution des deux déclencheurs (pour l'insertion puis pour la mise à jour) lorsque la ligne n'existe pas.

La bonne solution est

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

dans ce cas, une seule instruction est exécutée (lorsque la ligne existe ou non).

Andy
la source
1
Je vois ce que tu veux dire. Je mettrai à jour ma question. À propos, je ne sais pas pourquoi UPDATE OR IGNOREest nécessaire, car la mise à jour ne plantera pas si aucune ligne n'est trouvée.
bgusach
1
lisibilité? Je peux voir ce que fait le code d'Andy en un coup d'œil. Votre bgusach, j'ai dû étudier une minute pour comprendre.
Brandan
6

Pour avoir un UPSERT pur sans trous (pour les programmeurs) qui ne relaie pas sur des clés uniques et autres:

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

SELECT changes () renverra le nombre de mises à jour effectuées lors de la dernière demande. Ensuite, vérifiez si la valeur de retour de changes () est 0, si c'est le cas, exécutez:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 
Gilco
la source
C'est équivalent à ce que @fiznool proposait dans son commentaire (même si j'irais pour sa solution). Tout va bien et fonctionne correctement, mais vous n'avez pas d'instruction SQL unique. UPSERT non basé sur PK ou d'autres clés uniques n'a pas de sens pour moi.
bgusach
4

Vous pouvez également simplement ajouter une clause ON CONFLICT REPLACE à votre contrainte unique user_name, puis simplement INSÉRER, laissant à SQLite le soin de déterminer ce qu'il faut faire en cas de conflit. Voir: https://sqlite.org/lang_conflict.html .

Notez également la phrase concernant les déclencheurs de suppression: lorsque la stratégie de résolution de conflit REPLACE supprime des lignes afin de satisfaire une contrainte, les déclencheurs de suppression se déclenchent si et seulement si les déclencheurs récursifs sont activés.

Maximilian Tyrtania
la source
1

Option 1: Insérer -> Mettre à jour

Si vous aimez éviter à la fois changes()=0etINSERT OR IGNORE même si vous ne pouvez pas vous permettre de supprimer la ligne - Vous pouvez utiliser cette logique;

Tout d'abord, insérez (s'il n'existe pas), puis mettez à jour à en filtrant avec la clé unique.

Exemple

-- 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';

Concernant les déclencheurs

Remarque: je ne l'ai pas testé pour voir quels déclencheurs sont appelés, mais je suppose ce qui suit:

si la ligne n'existe pas

  • AVANT INSÉRER
  • INSÉRER avec AU LIEU DE
  • APRÈS INSÉRER
  • AVANT LA MISE À JOUR
  • MISE À JOUR avec AU LIEU DE
  • APRÈS LA MISE À JOUR

si la ligne existe

  • AVANT LA MISE À JOUR
  • MISE À JOUR avec AU LIEU DE
  • APRÈS LA MISE À JOUR

Option 2: insérer ou remplacer - conserver votre propre pièce d'identité

de cette façon, vous pouvez avoir une seule commande 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);

Modifier: option ajoutée 2.

itsho
la source