web-dev-qa-db-fra.com

SQLite UPSERT / UPDATE OR INSERT

Je dois exécuter UPSERT/INSERT OR UPDATE par rapport à une base de données SQLite.

Il y a la commande INSERT OR REPLACE qui peut être utile dans de nombreux cas. Mais si vous voulez conserver votre identifiant avec l'auto-incrémentation en place à cause de clés étrangères, cela ne fonctionnera pas car il effacera la ligne. , en crée un nouveau et, par conséquent, cette nouvelle ligne a un nouvel ID.

Ce serait la table:

players - (clé primaire sur id, nom d'utilisateur unique)

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

C'est une réponse tardive. Depuis SQLIte 3.24.0, publiée le 4 juin 2018, il existe enfin un support pour 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 indiquer cette réponse ci-dessous (posté par moi, @MarqueIV).

Toutefois, si vous avez la possibilité de mettre à niveau, vous êtes fortement encouragé à le faire, car contrairement à ma solution, celle publiée ici présente le comportement souhaité dans un environnement différent. déclaration unique. De plus, vous bénéficiez de toutes les autres fonctionnalités, améliorations et corrections de bogues fournies avec une version plus récente.

41
prapin

Q & A Style

Après des heures de recherche et de lutte contre le problème, j’ai découvert qu’il y avait deux façons de le faire, en fonction de la structure de votre table et des restrictions de clés étrangères activées pour préserver l’intégrité. Je voudrais partager ceci dans un format clair pour gagner du temps pour les personnes qui pourraient ê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é pour qu'il n'y ait pas d'exception d'intégrité. Le chemin à parcourir est INSERT OR REPLACE. Si vous essayez d'insérer/de mettre à jour un lecteur dont l'ID existe déjà, le moteur SQLite supprimera cette ligne et l'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 nom_utilisateur = '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)

Le truc est en fusion. Il retourne l'identifiant de l'utilisateur 'steven' le cas échéant, et sinon, il retourne un nouvel identifiant.


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

Après avoir fouillé avec la solution précédente, je me suis rendu compte que dans mon cas, cela pourrait finir par détruire des données, puisque cet identifiant fonctionne comme une clé étrangère pour une autre table. De plus, j'ai créé la table avec la clause ON DELETE CASCADE, ce qui voudrait dire qu'elle supprimerait les données en mode silencieux. Dangereux.

Donc, j'ai d'abord pensé à une clause IF, mais SQLite n'a que CASE . Et ce CAS ne peut pas être utilisé (ou du moins je ne l'ai pas géré) pour en effectuer une UPDATE requête si EXISTS (sélectionnez l'ID des lecteurs où nom_utilisateur = 'steven'), et INSÉREZ 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, pour chaque UPSERT que vous voulez exécuter, commencez par exécuter un INSERT OR 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 que précédemment: nom_utilisateur = '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!

MODIFIER

Comme Andy l’a commenté, essayer d’insérer d’abord puis de mettre à jour risque d’entraîner des déclenchements plus fréquents 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 user_name='steven', age=32 WHERE user_name='steven';

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

Voici une approche qui n'exige pas que la force brute "ignore" ne fonctionne que s'il y a une violation clé. Cette méthode fonctionne sur la base de toutes conditions spécifiées 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 "magie" est ici que vous utilisez la clause Where (Select Changes() = 0) pour déterminer s’il existe des lignes pour l’insertion, et puisque cela est basé sur votre propre clause Where, vous pouvez définir, pas seulement les violations clés.

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

Si le Updatea met à jour une ligne existante, alors Changes() = 1, de sorte que la clause 'Where' dans le Insert sera désormais fausse et ainsi aucun insert n'aura lieu.

Aucune force brute nécessaire.

66
MarqueIV

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 aux deux déclencheurs exécutés (pour l'insertion, puis pour la mise à jour) lorsque la ligne n'existe pas.

Une 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).

24
Andy

Pour avoir un UPSERT pur sans trous (pour les programmeurs) qui ne repose pas sur des clés uniques ou 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. Puis vérifiez si la valeur renvoyée par changes () est 0, si c'est le cas, exécutez:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 
4
Gilco

Vous pouvez également simplement ajouter une clause ON CONFLICT REPLACE à votre contrainte unique user_name, puis simplement INSERT, laissant le soin à SQLite de déterminer la marche à suivre 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 sont déclenchés si et seulement si les déclencheurs récursifs sont activés.

2
Maximilian Tyrtania

Option 1: Insérer -> Mettre à jour

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

D'abord, insert (s'il n'existe pas) et ensuite pdate 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
  • INSERT avec INSTEAD OF
  • APRÈS L'INSERT
  • AVANT MISE À JOUR
  • MISE À JOUR À L'AIDE D'INSTEAD OF
  • APRÈS LA MISE À JOUR

si la ligne existe

  • AVANT MISE À JOUR
  • MISE À JOUR À L'AIDE D'INSTEAD OF
  • APRÈS LA MISE À JOUR

Option 2: Insérer ou remplacer - conservez votre propre ID

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

Edit: ajout de l'option 2.

1
itsho