Question

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

Insérer la procédure stockée de mise à jour sur SQL Server

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

Fondamentalement, je souhaite mettre à jour trois colonnes sur quatre si l'enregistrement existe, s'il n'existe pas, je souhaite 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 à UPSERT.

(J'essaie d'éviter la surcharge de SELECT afin de déterminer si je dois évidemment mettre à JOUR ou INSÉRER)

Suggestions?


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

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 provoqueront pas de conflit, seul l'identifiant serait donc ASUSME BLOB1 et BLOB2 ne serait pas remplacé (comme souhaité)


Les mises à jour de 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 à l'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 en utilisant 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 d'instruction à l'aide de sqlite3_finalize().

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

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

Était-ce utile?

La solution

En supposant 3 colonnes dans la table. ID, NOM, ROLE

BAD: Ceci insère ou remplace toutes les colonnes avec les nouvelles valeurs pour ID = 1:

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

BAD: Ceci insère ou remplace 2 des colonnes ... la colonne NAME sera définie sur NULL ou la valeur par défaut:

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

BON: Cette opération mettra à jour 2 colonnes. Lorsque ID = 1 existe, le nom ne sera pas affecté. Si ID = 1 n'existe pas, le nom sera par défaut (NULL).

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

Ceci mettra à jour 2 des colonnes. Lorsque ID = 1 existe, le ROLE ne sera pas affecté. Si 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')
          );

Autres conseils

INSERT OR REPLACE n'est PAS équivalent à & "UPSERT &";.

.

Dites 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 du numéro d'employé 1. SQLite l'a remplacé par une valeur par défaut.

Le résultat attendu d'un UPSERT serait de changer le rôle et de conserver le nom.

La réponse de Eric B & # 8217; est OK si vous souhaitez en conserver un seul. ou peut-être deux colonnes de la ligne existante. Si vous souhaitez conserver un grand nombre de colonnes, la procédure devient trop lourde.

Voici & # 8217; une approche qui s'adapte parfaitement à n'importe quelle quantité de colonnes de chaque côté. Pour illustrer cela, supposons 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 name est la clé naturelle de la ligne & # 8211; id n’est utilisé que pour les clés étrangères. SQLite doit donc choisir lui-même la valeur de l’ID lors de l’insertion d’une nouvelle ligne. Mais lors de la mise à jour d'une ligne existante en fonction de son UPSERT, je souhaite qu'elle conserve l'ancien ID (évidemment!).

J'obtiens un vrai INSERT SELECT avec 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é consiste à utiliser old.id avec une jointure externe gauche pour joindre une ligne existante aux nouvelles valeurs.

Ici, si une ligne n'existait pas auparavant, NULL sera ts et SQLite attribuera alors automatiquement un ID, mais s'il existait déjà une telle ligne, DEFAULT aura une valeur réelle et être réutilisé. Ce qui est exactement ce que je voulais.

En fait, c'est très flexible. Notez que la new colonne est complètement absente de tous les côtés & # 8211; comme il a une valeur old, SQLite fera juste ce qu'il faut dans tous les cas, aussi je & # 8217; je ne dois pas m'en occuper moi-même.

Vous pouvez également inclure une colonne sur les côtés COALESCE(new.content, old.content) et SELECT, puis utiliser par exemple. <=> à l'extérieur <=> pour dire & # 8220; insérez le nouveau contenu, le cas échéant, sinon conservez l'ancien contenu & # 8221; & # 8211; par exemple. si vous utilisez une requête fixe et liez les nouvelles valeurs avec des espaces réservés.

Si vous faites généralement des mises à jour, je voudrais

  1. Commencer une transaction
  2. faire la mise à jour
  3. Vérifiez le nombre de lignes
  4. Si la valeur est 0, insérer
  5. S'engager

Si vous faites généralement des insertions, je le ferais

  1. Commencer une transaction
  2. Essayez une insertion
  3. Recherchez l'erreur de violation de clé primaire
  4. si nous avons une erreur faire la mise à jour
  5. S'engager

De cette façon, vous évitez la sélection et vous avez une sonorité transactionnelle sur SQLite.

Cette réponse a été mise à jour et les commentaires ci-dessous ne sont plus applicables.

2018-05-18 STOP PRESS.

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 en sorte que l'INSERT se comporte comme un UPDATE ou un non-op si l'INSERT enfreignait une contrainte d'unicité. UPSERT n'est pas un SQL standard. UPSERT dans SQLite suit la syntaxe établie par PostgreSQL.

 entrer la description de l'image ici

alternativement:

Une autre manière complètement différente de procéder est la suivante: dans mon application, je mets en rangée dans la mémoire ID la valeur long.MaxValue lorsque je crée la ligne en mémoire. (MaxValue ne sera jamais utilisé en tant qu'ID, vous ne vivrez pas assez longtemps .... Alors si rowID n'est pas cette valeur, il doit déjà être dans la base de données, il faut donc un UPDATE s'il s'agit de MaxValue, puis un insert. Cela n’est utile que si vous pouvez suivre les identifiants de ligne dans votre application.

Je réalise que c'est un vieux fil de discussion, mais je travaille depuis peu avec sqlite3 et je propose cette méthode qui répond mieux à mes besoins en matière 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>; 

Il reste encore 2 requêtes avec une clause where sur la mise à jour mais semble faire l'affaire. J'ai aussi en tête cette vision que sqlite peut optimiser complètement la déclaration de mise à jour si l'appel à changes () est supérieur à zéro. Que ce soit 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'identifiant de la ligne, qu'il s'agisse d'une nouvelle ligne insérée ou d'une ligne existante.

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;

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, un enregistrement existant a été mis à jour ou une insertion sera effectuée.

Le détail important est d'utiliser la fonction SQL changes () pour vérifier si l'instruction de mise à jour frappe des enregistrements existants et n'exécute l'instruction d'insertion que si elle n'a pas touché d'enregistrements.

Une chose à noter est que la fonction changes () ne renvoie pas les modifications effectuées par les déclencheurs de niveau inférieur (voir http://sqlite.org/lang_corefunc.html#changes ), veillez donc à en tenir compte.

Voici le code SQL ...

Tester la mise à jour:

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

Vous pouvez en effet faire un upsert dans SQLite, cela a juste l'air un peu différent de ce que vous avez l'habitude de faire. 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

Depuis 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 en sorte que l'INSERT se comporte comme une MISE À JOUR ou un non-op si l'INSERT violerait une contrainte d'unicité. UPSERT n'est pas un SQL standard. UPSERT dans SQLite respecte 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 suivie de la clause spéciale ON CONFLICT

 entrer la description de l'image ici

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

La meilleure approche que je connaisse consiste à effectuer une mise à jour, suivie d'un insert. Les & "; Frais généraux d'une sélection &"; est nécessaire, mais ce n’est pas un lourd fardeau puisque vous effectuez une recherche sur la clé primaire, ce qui est rapide.

Vous devriez pouvoir modifier les instructions ci-dessous avec votre table & amp; noms de champs 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
                );

Si quelqu'un veut lire ma solution pour SQLite dans Cordova, j'ai obtenu cette méthode js générique 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");
        });
    });
});
}

Alors, récupérez d’abord les noms de 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);
    });
});
}

Créez ensuite les transactions par programmation.

"Valeurs" est un tableau que vous devez créer auparavant et qui représente les lignes que vous souhaitez insérer ou mettre à jour dans le tableau.

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

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

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

Si vous définissez votre table comme suit:

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 UPDATE à la place de INSERT.

Hth ...

Cette méthode remixe quelques-unes des autres méthodes de answer in pour cette question et incorpore l'utilisation de CTE (Common Table Expressions). Je vais présenter la requête, puis expliquer pourquoi j'ai fait ce que j'ai fait.

J'aimerais 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, prenom, dernier nom

La requête est la suivante:

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. Comme 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 choisir les valeurs par défaut que vous souhaitez utiliser en remplaçant les valeurs NULL dans la fonction COALESCE par les valeurs qui devraient être.

Suivant Aristotle Pagaltzis et l'idée de COALESCE from La réponse de Eric B & # 8217; est une option upsert permettant de ne mettre à jour que quelques colonnes ou d'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 anciennes valeurs existantes et en insérant celles fournies lorsque le nom est introuvable:

REMARQUE id est forcé d'être NULL quand INSERT car il est supposé être auto-incrémenté. S'il ne s'agit que d'une clé primaire générée, vous pouvez également utiliser new.fieldname (voir Aristotle Pagaltzis comment ).

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 est la suivante: si vous souhaitez conserver les anciennes valeurs, utilisez <=>, lorsque vous souhaitez mettre à jour les valeurs, utilisez <=>

Après avoir lu ce fil de discussion et avoir été déçu qu'il ne soit pas facile de se contenter de cela! & "UPSERT &"; j'ai étudié plus avant ...

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

Au lieu d'utiliser: INSERT INTO

Utiliser: INSERT OR REPLACE INTO

Cela fait exactement ce que vous voulez faire!

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;
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top