SQLite - UPSERT * pas * INSÉRER ou REMPLACER

535

http://en.wikipedia.org/wiki/Upsert

Insérer le processus stocké de mise à jour sur SQL Server

Existe-t-il un moyen intelligent de le faire dans SQLite auquel je n'ai pas pensé?

Fondamentalement, je veux mettre à jour trois des quatre colonnes si l'enregistrement existe, s'il n'existe pas, je veux INSÉRER l'enregistrement avec la valeur par défaut (NUL) pour la quatrième colonne.

L'ID est une clé primaire, il n'y aura donc qu'un seul enregistrement sur UPSERT.

(J'essaie d'éviter la surcharge de SELECT afin de déterminer si j'ai besoin de METTRE À JOUR ou INSÉRER évidemment)

Suggestions?


Je ne peux pas confirmer cette syntaxe sur le site SQLite pour TABLE CREATE. Je n'ai pas construit de démo pour le tester, mais il ne semble pas être supporté ..

Si c'était le cas, j'ai trois colonnes, donc cela ressemblerait à:

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    Blob1 BLOB ON CONFLICT REPLACE, 
    Blob2 BLOB ON CONFLICT REPLACE, 
    Blob3 BLOB 
);

mais les deux premiers blobs ne causeront pas de conflit, seul l'ID serait donc je asusme Blob1 et Blob2 ne seraient pas remplacés (comme souhaité)


UPDATEs dans SQLite lorsque les données de liaison sont une transaction complète, ce qui signifie que chaque ligne envoyée à mettre à jour nécessite: Préparer / Lier / Étape / Finaliser les instructions contrairement à INSERT qui permet l'utilisation de la fonction de réinitialisation

La vie d'un objet de déclaration ressemble à ceci:

  1. Créez l'objet à l'aide de sqlite3_prepare_v2 ()
  2. Liez les valeurs aux paramètres de l'hôte à l'aide des interfaces sqlite3_bind_.
  3. Exécutez le SQL en appelant sqlite3_step ()
  4. Réinitialisez l'instruction à l'aide de sqlite3_reset (), puis revenez à l'étape 2 et répétez.
  5. Détruisez l'objet de déclaration à l'aide de sqlite3_finalize ().

UPDATE Je suppose que c'est lent par rapport à INSERT, mais comment se compare-t-il à SELECT en utilisant la clé primaire?

Peut-être devrais-je utiliser la sélection pour lire la 4e colonne (Blob3) puis utiliser REMPLACER pour écrire un nouvel enregistrement mélangeant la 4e colonne d'origine avec les nouvelles données pour les 3 premières colonnes?

Mike Trader
la source
5
SQLite - UPSERT disponible en préversion consulter: sqlite.1065341.n5.nabble.com/...
Gaurav
3
UPSERT disponible dans la version 3.24.0 de SQLite
pablo_worker

Réponses:

854

En supposant trois colonnes dans le tableau: ID, NAME, ROLE


MAUVAIS: Cela insérera ou remplacera toutes les colonnes par de nouvelles valeurs pour ID = 1:

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (1, 'John Foo', 'CEO');

MAUVAIS: Cela insérera ou remplacera 2 des colonnes ... la colonne NOM sera définie sur NULL ou la valeur par défaut:

INSERT OR REPLACE INTO Employee (id, role) 
  VALUES (1, 'code monkey');

BON : utilisez la prise en charge UPSERT de la clause de conflit SQLite On dans SQLite! La syntaxe UPSERT a été ajoutée à SQLite avec la version 3.24.0!

UPSERT est une addition de syntaxe spéciale à INSERT qui fait que INSERT se comporte comme une MISE À JOUR ou un no-op si INSERT viole une contrainte d'unicité. UPSERT n'est pas du SQL standard. UPSERT dans SQLite suit la syntaxe établie par PostgreSQL.

entrez la description de l'image ici

BON mais tendu: cela mettra à jour 2 des colonnes. Lorsque ID = 1 existe, le NOM n'est pas affecté. Lorsque ID = 1 n'existe pas, le nom sera la valeur par défaut (NULL).

INSERT OR REPLACE INTO Employee (id, role, name) 
  VALUES (  1, 
            'code monkey',
            (SELECT name FROM Employee WHERE id = 1)
          );

Cela mettra à jour 2 des colonnes. Lorsque ID = 1 existe, le RÔLE ne sera pas affecté. Lorsque ID = 1 n'existe pas, le rôle sera défini sur «Benchwarmer» au lieu de la valeur par défaut.

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (  1, 
            'Susan Bar',
            COALESCE((SELECT role FROM Employee WHERE id = 1), 'Benchwarmer')
          );
Eric B
la source
33
+1 brillant! La clause de sélection intégrée vous donne la possibilité de remplacer la fonctionnalité ON CONFLICT REPLACE par défaut si vous devez combiner / comparer l'ancienne valeur et la nouvelle valeur pour n'importe quel champ.
G__
24
Si l'employé est référencé par d'autres lignes avec suppression en cascade, les autres lignes seront toujours supprimées par remplacement.
Don Reba
246
Cela fait mal. SQlite a besoin d'UPSERT.
andig
21
Pourriez-vous expliquer pourquoi cela va insérer ou remplacer toutes les colonnes par de nouvelles valeurs pour ID = 1: est considéré comme MAUVAIS dans votre premier exemple? La commande que vous y présentez est destinée à créer un nouvel enregistrement avec l'ID 1, nom John Foo et rôle PDG , ou écraser l'enregistrement dont l'ID est 1, s'il est déjà là, avec ces données (en supposant que l' ID est la clé primaire) . Alors, pourquoi est-ce mauvais si cela se produit exactement?
OR Mapper
10
@Cornelius: C'est clair, mais ce n'est pas ce qui se passe dans le premier exemple. Le premier exemple est destiné à forcer la définition de toutes les colonnes, ce qui se produit exactement, que l'enregistrement soit inséré ou remplacé. Alors, pourquoi est-ce considéré comme mauvais? La réponse liée indique également uniquement pourquoi quelque chose de mauvais peut se produire lors de la spécification d'un sous-ensemble des colonnes, comme dans votre deuxième exemple; il ne semble pas développer les mauvais effets de ce qui se passe dans votre premier exemple, INSERT OR REPLACEtout en spécifiant des valeurs pour toutes les colonnes.
OR Mapper du
134

INSÉRER OU REMPLACER N'EST PAS équivalent à "UPSERT".

Supposons que j'ai la table Employee avec les champs id, name et role:

INSERT OR REPLACE INTO Employee ("id", "name", "role") VALUES (1, "John Foo", "CEO")
INSERT OR REPLACE INTO Employee ("id", "role") VALUES (1, "code monkey")

Boom, vous avez perdu le nom de l'employé numéro 1. SQLite l'a remplacé par une valeur par défaut.

La sortie attendue d'un UPSERT serait de changer le rôle et de conserver le nom.

gregschlom
la source
21
-1 de moi j'ai peur. Oui, la réponse acceptée est fausse, mais bien que votre réponse souligne le problème, ce n'est pas non plus une réponse. Pour une réponse réelle, voir la solution intelligente d'Eric B utilisant une coalesce((select ..), 'new value')clause intégrée . Je pense que la réponse d'Eric a besoin de plus de votes.
Jour
22
En effet. Eric est définitivement la meilleure réponse et mérite plus de votes. Cela étant dit, je pense qu'en soulignant le problème, j'ai contribué un peu à trouver la bonne réponse (la réponse d'Eric est venue plus tard et s'appuie sur les exemples de tables sql dans ma réponse). Donc, je ne sais pas si je mérite un -1, mais peu importe :)
gregschlom
6
+1 pour compenser le -1 ci-dessus. lol. Chronologie intéressante sur ce sujet. De toute évidence, votre réponse a eu lieu plus d'une semaine avant celle d'Eric, mais vous avez tous les deux répondu à la question près de deux ans après qu'elle a été posée. +1 pour Eric aussi pour l'élaboration.
Rich
2
@QED non, car delete + insert (qui est un remplacement) est 2 instructions dml, avec leurs propres déclencheurs par exemple. Ce n'est pas la même chose qu'une seule déclaration de mise à jour.
Sebas
109

La réponse d'Eric B est OK si vous souhaitez conserver une ou peut-être deux colonnes de la ligne existante. Si vous souhaitez conserver un grand nombre de colonnes, cela devient trop lourd rapidement.

Voici une approche qui s'adapte bien à n'importe quel nombre de colonnes de chaque côté. Pour l'illustrer, je supposerai le schéma suivant:

 CREATE TABLE page (
     id      INTEGER PRIMARY KEY,
     name    TEXT UNIQUE,
     title   TEXT,
     content TEXT,
     author  INTEGER NOT NULL REFERENCES user (id),
     ts      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );

Notez en particulier que namec'est la clé naturelle de la ligne - idn'est utilisée que pour les clés étrangères, donc le point est pour SQLite de choisir la valeur ID elle-même lors de l'insertion d'une nouvelle ligne. Mais lors de la mise à jour d'une ligne existante en fonction de son name, je veux qu'elle continue d'avoir l'ancienne valeur ID (évidemment!).

J'atteins un vrai UPSERTavec la construction suivante:

 WITH new (name, title, author) AS ( VALUES('about', 'About this site', 42) )
 INSERT OR REPLACE INTO page (id, name, title, content, author)
 SELECT old.id, new.name, new.title, old.content, new.author
 FROM new LEFT JOIN page AS old ON new.name = old.name;

La forme exacte de cette requête peut varier un peu. La clé est l'utilisation de INSERT SELECTavec une jointure externe gauche, pour joindre une ligne existante aux nouvelles valeurs.

Ici, si une ligne n'existait pas auparavant, ce old.idsera le cas NULLet SQLite attribuera alors un ID automatiquement, mais s'il y avait déjà une telle ligne, old.idaura une valeur réelle et celle-ci sera réutilisée. C'est exactement ce que je voulais.

En fait, c'est très flexible. Notez que la tscolonne manque complètement de tous les côtés - car elle a une DEFAULTvaleur, SQLite fera juste la bonne chose dans tous les cas, donc je n'ai pas à m'en occuper moi-même.

Vous pouvez également inclure une colonne sur les deux newet les oldcôtés et puis utiliser par exemple COALESCE(new.content, old.content)dans la partie extérieure SELECTde dire « insérer le nouveau contenu s'il y avait, sinon garder l'ancien contenu » - par exemple , si vous utilisez une requête fixe et liez la nouvelle valeurs avec des espaces réservés.

Aristote Pagaltzis
la source
13
+1, fonctionne très bien, mais ajoutez une WHERE name = "about"contrainte sur le SELECT ... AS oldpour accélérer les choses. Si vous avez 1m + de lignes, c'est très lent.
Bon point, +1 sur votre commentaire. Je laisserai cela de côté pour la réponse, car l'ajout d'une telle WHEREclause nécessite simplement le type de redondance dans la requête que j'essayais d'éviter en premier lieu lorsque j'ai proposé cette approche. Comme toujours: lorsque vous avez besoin de performances, dénormalisez - la structure de la requête, dans ce cas.
Aristote Pagaltzis le
4
Vous pouvez simplifier l'exemple d'Aristote jusqu'à ceci, si vous le souhaitez: INSERT OR REPLACE INTO page (id, name, title, content, author) SELECT id, 'about', 'About this site', content, 42 FROM ( SELECT NULL ) LEFT JOIN ( SELECT * FROM page WHERE name = 'about' )
jcox
4
Cela ne ON DELETEdéclencherait-il pas inutilement des déclencheurs lorsqu'il effectue un remplacement (c'est-à-dire une mise à jour)?
Karakuri
3
Cela déclenchera certainement des ON DELETEdéclencheurs. Je ne sais pas inutilement. Pour la plupart des utilisateurs, ce serait probablement inutile, même indésirable, mais peut-être pas pour tous les utilisateurs. De même pour le fait qu'il supprimera également en cascade toutes les lignes avec des clés étrangères dans la ligne en question - probablement un problème pour de nombreux utilisateurs. Malheureusement, SQLite n'a rien de plus proche d'un véritable UPSERT. (Sauf pour le simuler avec un INSTEAD OF UPDATEdéclencheur, je suppose.)
Aristote Pagaltzis
86

Si vous faites généralement des mises à jour, je le ferais ..

  1. Commencer une transaction
  2. Faites la mise à jour
  3. Vérifiez le nombre de lignes
  4. Si c'est 0 faites l'insert
  5. Commettre

Si vous faites généralement des encarts, je

  1. Commencer une transaction
  2. Essayez un insert
  3. Vérifier l'erreur de violation de clé primaire
  4. si nous avons une erreur, faites la mise à jour
  5. Commettre

De cette façon, vous évitez la sélection et vous êtes transactionnellement solide sur Sqlite.

Sam Saffron
la source
Merci, venez de poser cette @ stackoverflow.com/questions/2717590/… . +1 de moi! =)
Alix Axel
4
Si vous allez vérifier le nombre de lignes à l'aide de sqlite3_changes () à la 3e étape, assurez-vous de ne pas utiliser le descripteur de base de données de plusieurs threads pour les modifications.
Linulin
3
Les éléments suivants ne seraient-ils pas encore moins verbeux avec le même effet: 1) sélectionnez la table de formulaire d'identification où id = 'x' 2) si (ResultSet.rows.length == 0) mettez à jour la table où id = 'x';
Florin
20
Je souhaite vraiment que INSERT OR UPDATE fasse partie de la langue
Florin
Cela a fini par fonctionner le mieux pour moi, en essayant de faire REPLACE INTO ou toujours et l'insertion / mise à jour tuait mes performances lorsque je faisais principalement des mises à jour au lieu d'inserts.
PherricOxide
83

Cette réponse a été mise à jour et les commentaires ci-dessous ne s'appliquent donc plus.

2018-05-18 STOP PRESSE.

Prise en charge UPSERT dans SQLite!La syntaxe UPSERT a été ajoutée à SQLite avec la version 3.24.0 (en attente)!

UPSERT est une addition de syntaxe spéciale à INSERT qui fait que INSERT se comporte comme une MISE À JOUR ou un no-op si INSERT viole une contrainte d'unicité. UPSERT n'est pas du SQL standard. UPSERT dans SQLite suit la syntaxe établie par PostgreSQL.

entrez la description de l'image ici

alternativement:

Une autre façon complètement différente de procéder est la suivante: dans mon application, j'ai défini mon en ligne rowID pour qu'il soit long.MaxValue lorsque je crée la ligne en mémoire. (MaxValue ne sera jamais utilisé comme un identifiant, vous ne vivrez pas assez longtemps .... Ensuite, si rowID n'est pas cette valeur, il doit déjà être dans la base de données, il a donc besoin d'une MISE À JOUR s'il s'agit de MaxValue, alors il a besoin d'un insert. Cela n'est utile que si vous pouvez suivre les ID de ligne dans votre application.

AnthonyLambert
la source
5
Amen. Simple, c'est mieux que complexe. Cela semble un peu plus simple que la réponse acceptée.
kkurian
4
Je pensais que vous ne pouviez pas faire INSERER DANS ... OERE dans sqlite? Ceci est une erreur de syntaxe pour moi dans sqlite3
Charlie Martin
5
@CharlieMartin a raison. Cette syntaxe n'est pas valide pour SQLite - c'est ce que l'OP a demandé. Une WHEREclause ne peut pas être ajoutée à une INSERTinstruction: sqlite-insert ...
colm.anseo
4
Cette réponse m'a fait perdre beaucoup de temps, la question concerne SQLITE et je ne savais pas que INSERER OERE n'était pas pris en charge dans sqite, veuillez ajouter cette note qu'elle n'est pas valide pour sqlite dans votre réponse
Miquel
3
@colminator et autres: J'ai corrigé sa déclaration SQL en utilisant votre suggestion.
F Lekschas
62

Je me rends compte que c'est un vieux thread mais j'ai travaillé dans sqlite3 récemment et j'ai trouvé cette méthode qui correspondait mieux à mes besoins de génération dynamique de requêtes paramétrées:

insert or ignore into <table>(<primaryKey>, <column1>, <column2>, ...) values(<primaryKeyValue>, <value1>, <value2>, ...); 
update <table> set <column1>=<value1>, <column2>=<value2>, ... where changes()=0 and <primaryKey>=<primaryKeyValue>; 

C'est toujours 2 requêtes avec une clause where sur la mise à jour mais semble faire l'affaire. J'ai également cette vision en tête que sqlite peut optimiser complètement l'instruction de mise à jour si l'appel à changes () est supérieur à zéro. Qu'il le fasse ou non, cela dépasse mes connaissances, mais un homme peut rêver n'est-ce pas? ;)

Pour les points bonus, vous pouvez ajouter cette ligne qui vous renvoie l'id de la ligne, que ce soit une ligne nouvellement insérée ou une ligne existante.

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;
Chris Stavropoulos
la source
+ 1. Exactement ce que j'essayais de remplacer en faisant une conversion à partir d'un code SQL Server TSQL. Merci. SQL que j'essayais de remplacer était commeUpdate <statement> If @@ROWCOUNT=0 INSERT INTO <statement>
DarrenMB
14

Voici une solution qui est vraiment un UPSERT (UPDATE ou INSERT) au lieu d'un INSERT OR REPLACE (qui fonctionne différemment dans de nombreuses situations).

Cela fonctionne comme ceci:
1. Essayez de mettre à jour si un enregistrement avec le même ID existe.
2. Si la mise à jour n'a modifié aucune ligne (NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0) ), insérez l'enregistrement.

Donc, soit un enregistrement existant a été mis à jour, soit une insertion sera effectuée.

Le détail important consiste à utiliser la fonction SQL changes () pour vérifier si l'instruction de mise à jour a atteint des enregistrements existants et à exécuter l'instruction d'insertion uniquement si elle n'a atteint aucun enregistrement.

Une chose à mentionner est que la fonction changes () ne renvoie pas les modifications effectuées par des déclencheurs de niveau inférieur (voir http://sqlite.org/lang_corefunc.html#changes ), alors assurez-vous de prendre cela en compte.

Voici le SQL ...

Mise à jour du test:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 2;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 2, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;

Insert de test:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 3;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 3, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;
David Liebeherr
la source
2
Cela semble être la meilleure solution pour moi que celles d'Eric. Cependant, cela INSERT INTO Contact (Id, Name) SELECT 3, 'Bob' WHERE changes() = 0;devrait également fonctionner.
bkausbk
Merci mec, ça marche aussi en WebSQL (en utilisant le plugin Cordova et SQLite)
Zappescu
11

À partir de la version 3.24.0, UPSERT est pris en charge par SQLite.

De la documentation :

UPSERT est une addition de syntaxe spéciale à INSERT qui fait que INSERT se comporte comme une MISE À JOUR ou un no-op si INSERT viole une contrainte d'unicité. UPSERT n'est pas du SQL standard. UPSERT dans SQLite suit la syntaxe établie par PostgreSQL. La syntaxe UPSERT a été ajoutée à SQLite avec la version 3.24.0 (en attente).

Un UPSERT est une instruction INSERT ordinaire qui est suivie de la clause spéciale ON CONFLICT

entrez la description de l'image ici

Source de l'image: https://www.sqlite.org/images/syntax/upsert-clause.gif

Lukasz Szozda
la source
8

Vous pouvez en effet faire un upsert dans SQLite, il semble juste un peu différent de celui auquel vous êtes habitué. Cela ressemblerait à quelque chose comme:

INSERT INTO table name (column1, column2) 
VALUES ("value12", "value2") WHERE id = 123 
ON CONFLICT DO UPDATE 
SET column1 = "value1", column2 = "value2" WHERE id = 123
Brill Pappin
la source
5

Développer la réponse d'Aristote vous pouvez sélectionner à partir d'une table factice «singleton» (une table de votre propre création avec une seule ligne). Cela évite une certaine duplication.

J'ai également gardé l'exemple portable sur MySQL et SQLite et utilisé une colonne 'date_added' comme exemple de la façon dont vous pouvez définir une colonne uniquement la première fois.

 REPLACE INTO page (
   id,
   name,
   title,
   content,
   author,
   date_added)
 SELECT
   old.id,
   "about",
   "About this site",
   old.content,
   42,
   IFNULL(old.date_added,"21/05/2013")
 FROM singleton
 LEFT JOIN page AS old ON old.name = "about";
Communauté
la source
4

La meilleure approche que je connaisse est de faire une mise à jour, suivie d'une insertion. Le "surcoût d'une sélection" est nécessaire, mais ce n'est pas un fardeau terrible puisque vous recherchez sur la clé primaire, ce qui est rapide.

Vous devriez pouvoir modifier les instructions ci-dessous avec vos noms de table et de champ pour faire ce que vous voulez.

--first, update any matches
UPDATE DESTINATION_TABLE DT
SET
  MY_FIELD1 = (
              SELECT MY_FIELD1
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
 ,MY_FIELD2 = (
              SELECT MY_FIELD2
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
WHERE EXISTS(
            SELECT ST2.PRIMARY_KEY
            FROM
              SOURCE_TABLE ST2
             ,DESTINATION_TABLE DT2
            WHERE ST2.PRIMARY_KEY = DT2.PRIMARY_KEY
            );

--second, insert any non-matches
INSERT INTO DESTINATION_TABLE(
  MY_FIELD1
 ,MY_FIELD2
)
SELECT
  ST.MY_FIELD1
 ,NULL AS MY_FIELD2  --insert NULL into this field
FROM
  SOURCE_TABLE ST
WHERE NOT EXISTS(
                SELECT DT2.PRIMARY_KEY
                FROM DESTINATION_TABLE DT2
                WHERE DT2.PRIMARY_KEY = ST.PRIMARY_KEY
                );
JosephStyons
la source
7
Manière de compliqué.
2010 à 7h17
Je pense que ce n'est pas une bonne idée car vous devez faire deux fois une requête au moteur de base de données.
Ricardo da Rocha Vitor
3

Si quelqu'un veut lire ma solution pour SQLite à Cordova, j'ai obtenu cette méthode générique js grâce à la réponse @david ci-dessus.

function    addOrUpdateRecords(tableName, values, callback) {
get_columnNames(tableName, function (data) {
    var columnNames = data;
    myDb.transaction(function (transaction) {
        var query_update = "";
        var query_insert = "";
        var update_string = "UPDATE " + tableName + " SET ";
        var insert_string = "INSERT INTO " + tableName + " SELECT ";
        myDb.transaction(function (transaction) {
            // Data from the array [[data1, ... datan],[()],[()]...]:
            $.each(values, function (index1, value1) {
                var sel_str = "";
                var upd_str = "";
                var remoteid = "";
                $.each(value1, function (index2, value2) {
                    if (index2 == 0) remoteid = value2;
                    upd_str = upd_str + columnNames[index2] + "='" + value2 + "', ";
                    sel_str = sel_str + "'" + value2 + "', ";
                });
                sel_str = sel_str.substr(0, sel_str.length - 2);
                sel_str = sel_str + " WHERE NOT EXISTS(SELECT changes() AS change FROM "+tableName+" WHERE change <> 0);";
                upd_str = upd_str.substr(0, upd_str.length - 2);
                upd_str = upd_str + " WHERE remoteid = '" + remoteid + "';";                    
                query_update = update_string + upd_str;
                query_insert = insert_string + sel_str;  
                // Start transaction:
                transaction.executeSql(query_update);
                transaction.executeSql(query_insert);                    
            });
        }, function (error) {
            callback("Error: " + error);
        }, function () {
            callback("Success");
        });
    });
});
}

Donc, prenez d'abord les noms des colonnes avec cette fonction:

function get_columnNames(tableName, callback) {
myDb.transaction(function (transaction) {
    var query_exec = "SELECT name, sql FROM sqlite_master WHERE type='table' AND name ='" + tableName + "'";
    transaction.executeSql(query_exec, [], function (tx, results) {
        var columnParts = results.rows.item(0).sql.replace(/^[^\(]+\(([^\)]+)\)/g, '$1').split(','); ///// RegEx
        var columnNames = [];
        for (i in columnParts) {
            if (typeof columnParts[i] === 'string')
                columnNames.push(columnParts[i].split(" ")[0]);
        };
        callback(columnNames);
    });
});
}

Générez ensuite les transactions par programme.

"Valeurs" est un tableau que vous devez créer avant et il représente les lignes que vous souhaitez insérer ou mettre à jour dans la table.

"remoteid" est l'identifiant que j'ai utilisé comme référence, car je me synchronise avec mon serveur distant.

Pour l'utilisation du plugin SQLite Cordova, veuillez vous référer au lien officiel

Zappescu
la source
2

Cette méthode remixe quelques-unes des autres méthodes de réponse dans cette question et intègre l'utilisation de CTE (Common Table Expressions). Je vais introduire la requête puis expliquer pourquoi j'ai fait ce que j'ai fait.

Je voudrais changer le nom de famille de l'employé 300 en DAVIS s'il y a un employé 300. Sinon, j'ajouterai un nouvel employé.

Nom de la table: employés Colonnes: id, prénom, nom

La requête est:

INSERT OR REPLACE INTO employees (employee_id, first_name, last_name)
WITH registered_employees AS ( --CTE for checking if the row exists or not
    SELECT --this is needed to ensure that the null row comes second
        *
    FROM (
        SELECT --an existing row
            *
        FROM
            employees
        WHERE
            employee_id = '300'

        UNION

        SELECT --a dummy row if the original cannot be found
            NULL AS employee_id,
            NULL AS first_name,
            NULL AS last_name
    )
    ORDER BY
        employee_id IS NULL --we want nulls to be last
    LIMIT 1 --we only want one row from this statement
)
SELECT --this is where you provide defaults for what you would like to insert
    registered_employees.employee_id, --if this is null the SQLite default will be used
    COALESCE(registered_employees.first_name, 'SALLY'),
    'DAVIS'
FROM
    registered_employees
;

Fondamentalement, j'ai utilisé le CTE pour réduire le nombre de fois où l'instruction select doit être utilisée pour déterminer les valeurs par défaut. Puisqu'il s'agit d'un CTE, nous sélectionnons simplement les colonnes que nous voulons dans la table et l'instruction INSERT l'utilise.

Vous pouvez maintenant décider quelles valeurs par défaut vous souhaitez utiliser en remplaçant les valeurs nulles, dans la fonction COALESCE par ce que les valeurs devraient être.

Dodzi Dzakuma
la source
2

À la suite d' Aristote Pagaltzis et de l'idée de la réponseCOALESCE d' Eric B , voici une option positive pour mettre à jour seulement quelques colonnes ou insérer une ligne complète si elle n'existe pas.

Dans ce cas, imaginez que le titre et le contenu doivent être mis à jour, en conservant les autres anciennes valeurs lors de l'existant et en insérant celles fournies lorsque le nom est introuvable:

NOTE id est forcé d'être NULL quand INSERTil est censé être auto-incrémenté. S'il ne s'agit que d'une clé primaire générée, elle COALESCEpeut également être utilisée (voir le commentaire d'Aristote Pagaltzis ).

WITH new (id, name, title, content, author)
     AS ( VALUES(100, 'about', 'About this site', 'Whatever new content here', 42) )
INSERT OR REPLACE INTO page (id, name, title, content, author)
SELECT
     old.id, COALESCE(old.name, new.name),
     new.title, new.content,
     COALESCE(old.author, new.author)
FROM new LEFT JOIN page AS old ON new.name = old.name;

Donc, la règle générale serait, si vous souhaitez conserver les anciennes valeurs, utilisez COALESCE, lorsque vous souhaitez mettre à jour les valeurs, utiliseznew.fieldname

Miquel
la source
COALESCE(old.id, new.id)est définitivement faux avec une clé d'auto-incrémentation. Et bien que «garder la plupart des lignes inchangées, sauf là où des valeurs manquent» ressemble à un cas d'utilisation que quelqu'un pourrait avoir, je ne pense pas que ce soit ce que les gens recherchent lorsqu'ils cherchent à faire un UPSERT.
Aristote Pagaltzis
@AristotlePagaltzis s'excuse si je me trompe, je n'utilise pas d'auto-incréments. Ce que je recherche avec cette requête est de ne mettre à jour que quelques colonnes ou d'insérer une ligne complète avec les valeurs fournies au cas où elle n'existerait pas . J'ai joué avec votre requête et je n'ai pas pu y parvenir lors de l'insertion: les colonnes sélectionnées dans la oldtable où elles étaient affectées NULL, pas aux valeurs fournies dans new. C'est la raison d'utiliser COALESCE. Je ne suis pas un expert en sqlite, j'ai testé cette requête et semble fonctionner pour le cas, je vous remercie beaucoup si vous pouviez me pointer vers la solution avec auto
Miquel
2
Dans le cas d'une clé à incrémentation automatique, vous souhaitez l' insérer NULLcomme clé, car cela indique à SQLite d'insérer à la place la prochaine valeur disponible.
Aristote Pagaltzis
Je ne dis pas que vous ne devriez pas faire ce que vous faites (si vous en avez besoin, vous en avez besoin), juste que ce n'est pas vraiment une réponse à la question ici. UPSERT signifie généralement que vous avez une ligne que vous souhaitez stocker dans le tableau et que vous ne savez tout simplement pas si vous avez déjà une ligne correspondante dans laquelle placer les valeurs ou si vous devez les insérer en tant que nouvelle ligne. Votre cas d'utilisation est que si vous avez déjà une ligne correspondante, vous souhaitez ignorer la plupart des valeurs de la nouvelle ligne . C'est bien, ce n'est tout simplement pas la question qui a été posée.
Aristote Pagaltzis
1

Je pense que c'est peut-être ce que vous recherchez: clause ON CONFLICT .

Si vous définissez votre table comme ceci:

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    field1 TEXT 
); 

Maintenant, si vous faites un INSERT avec un identifiant qui existe déjà, SQLite effectue automatiquement la MISE À JOUR au lieu de INSERT.

Hth ...

kmelvn
la source
6
Je ne pense pas que cela fonctionne, cela effacera les colonnes manquantes de la déclaration d'insertion
Sam Saffron
3
@Mosor: -1 de ma part, désolé. Cela revient à émettre une REPLACEdéclaration.
Alix Axel
7
-1 car cela fait une suppression puis une insertion si la clé primaire existe déjà.
2010 à 7h13
1

Si cela ne vous dérange pas de le faire en deux opérations.

Pas:

1) Ajoutez de nouveaux éléments avec "INSÉRER OU IGNORER"

2) Mettre à jour les éléments existants avec "UPDATE"

L'entrée des deux étapes est la même collection d'éléments nouveaux ou pouvant être mis à jour. Fonctionne bien avec les éléments existants qui ne nécessitent aucune modification. Ils seront mis à jour, mais avec les mêmes données et donc le résultat net est sans changement.

Bien sûr, plus lent, etc. Inefficace. Oui.

Facile à écrire le sql et à le maintenir et le comprendre? Absolument.

C'est un compromis à considérer. Fonctionne très bien pour les petits upserts. Fonctionne très bien pour ceux qui ne craignent pas de sacrifier l'efficacité pour la maintenabilité du code.

sapbucket
la source
Note à tout le monde: INSÉRER OU REMPLACER N'EST PAS de quoi parle ce message. INSÉRER OU REMPLACER créera une NOUVELLE ligne dans votre table, avec un nouvel ID. Ce n'est pas une MISE À JOUR.
sapbucket
-2

Ayant juste lu ce fil et déçu qu'il n'était pas facile de se contenter de cette ing "UPSERT", j'ai approfondi ...

Vous pouvez réellement le faire directement et facilement dans SQLITE.

À la place d'utiliser: INSERT INTO

Utilisation: INSERT OR REPLACE INTO

Cela fait exactement ce que vous voulez qu'il fasse!

CFF
la source
21
-1 INSERT OR REPLACEn'est pas un UPSERT. Voir la "réponse" de gregschlom pour la raison. La solution d'Eric B fonctionne et a besoin de quelques votes positifs.
Jour
-4
SELECT COUNT(*) FROM table1 WHERE id = 1;

si COUNT(*) = 0

INSERT INTO table1(col1, col2, cole) VALUES(var1,var2,var3);

sinon si COUNT(*) > 0

UPDATE table1 SET col1 = var4, col2 = var5, col3 = var6 WHERE id = 1;
mjb
la source
C'est beaucoup trop compliqué, SQL peut gérer cela très bien en une seule requête
Robin Kanters