SQLiteデータベースに対してUPSERT/INSERT OR UPDATEを実行する必要があります。
INSERT OR REPLACEコマンドがあり、多くの場合に役立ちます。ただし、外部キーのために自動インクリメントを使用してIDを保持したい場合、行を削除し、新しい行を作成し、その結果、この新しい行には新しいIDが設定されるため機能しません。
これはテーブルになります:
プレーヤー-(IDの主キー、一意のuser_name)
| id | user_name | age |
------------------------------
| 1982 | johnny | 23 |
| 1983 | steven | 29 |
| 1984 | pepee | 40 |
これは遅い回答です。 2018年6月4日にリリースされたSQLIte 3.24.0以降、PostgreSQL構文に続く UPSERT 句が最終的にサポートされます。
INSERT INTO players (user_name, age)
VALUES('steven', 32)
ON CONFLICT(user_name)
DO UPDATE SET age=excluded.age;
注:3.24.0より前のバージョンのSQLiteを使用する必要がある場合は、 この回答 を参照してください(投稿者:@MarqueIV)。
ただし、アップグレードするオプションがある場合は、強くお勧めします、私のソリューションとは異なり、ここに投稿されたものは、単一のステートメント。さらに、通常は最新リリースに付属する他のすべての機能、改善、およびバグ修正を取得できます。
さて、何時間も問題を調査し、戦った後、テーブルの構造と、整合性を維持するために外部キーの制限を有効にするかどうかに応じて、これを達成する2つの方法があることがわかりました。私の状況にいる人々に時間を節約するために、これをきれいな形式で共有したいと思います。
つまり、外部キーがないか、または外部キーがある場合、SQLiteエンジンは整合性例外がないように構成されます。方法はINSERT OR REPLACEです。 IDが既に存在するプレーヤーを挿入/更新しようとすると、SQLiteエンジンはその行を削除し、提供しているデータを挿入します。ここで質問が来ます:古いIDを関連付けたままにするために何をすべきか?
データuser_name = 'steven'およびage = 32でUPSERTにしたいとしましょう。
このコードを見てください:
INSERT INTO players (id, name, age)
VALUES (
coalesce((select id from players where user_name='steven'),
(select max(id) from drawings) + 1),
32)
トリックは合体です。ユーザー 'steven'のIDを返します(存在しない場合)。それ以外の場合は、新しい新しいIDを返します。
前のソリューションを試した後、この場合、このIDは他のテーブルの外部キーとして機能するため、データが破壊される可能性があることに気付きました。また、ON DELETE CASCADE句を使用してテーブルを作成しました。これは、データをサイレントに削除することを意味します。危険な。
したがって、私は最初にIF句を考えましたが、SQLiteにはCASEしかありません。そして、これはCASEを使用して1つ実行することはできません(または少なくとも管理していません)UPDATE EXISTS(user_name = 'steven')、およびINSERT含まれていない場合。立ち入り禁止。
そして、ついに私はブルートフォースを使用して成功しました。ロジックは、実行する各UPSERTに対して、最初にINSERT OR IGNOREを実行して、行があることを確認しますユーザー、そして挿入しようとしたのとまったく同じデータでUPDATEクエリを実行します。
前と同じデータ:user_name = 'steven'および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';
そして、それだけです!
Andyがコメントしたように、最初に挿入してから更新しようとすると、予想よりも頻繁にトリガーが起動される可能性があります。私の意見ではこれはデータの安全性の問題ではありませんが、不必要なイベントの発生はほとんど意味がないことは事実です。したがって、改善されたソリューションは次のようになります。
-- 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);
これは、キー違反があった場合にのみ機能するブルートフォースの「無視」を必要としないアプローチです。この方法は、更新で指定するany条件に基づいて機能します。
これを試して...
-- 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);
ここでの「魔法」は、挿入する行があるかどうかを判断するためにWhere (Select Changes() = 0)
句を使用することです。これは独自のWhere
句に基づいているため、定義するものではなく、キー違反のみ。
上記の例では、更新による変更がない場合(つまり、レコードが存在しない場合)Changes()
= 0であるため、Where
ステートメントのInsert
句はtrueを返し、指定したデータで新しい行が挿入されます。
Update
didが既存の行を更新する場合、Changes()
= 1になるので、Insert
の 'Where'句はfalseになり、挿入されません。開催されます。
総当たり攻撃は必要ありません。
提示されたすべての答えの問題は、トリガー(およびおそらく他の副作用)を考慮に入れていないという完全な欠如です。のようなソリューション
INSERT OR IGNORE ...
UPDATE ...
行が存在しない場合、両方のトリガーが実行されます(挿入および更新)。
適切なソリューションは
UPDATE OR IGNORE ...
INSERT OR IGNORE ...
その場合、1つのステートメントのみが実行されます(行が存在するかどうか)。
一意のキーや他のキーで中継しない(プログラマ向けの)穴のない純粋なUPSERTを作成するには:
UPDATE players SET user_name="gil", age=32 WHERE user_name='george';
SELECT changes();
SELECT changes()は、最後の問い合わせで行われた更新の数を返します。次に、changes()からの戻り値が0であるかどうかを確認し、そうであれば実行します。
INSERT INTO players (user_name, age) VALUES ('gil', 32);
また、ON CONFLICT REPLACE句をuser_name一意制約に追加してから、挿入するだけでSQLiteに残して、競合が発生した場合の対処方法を把握することもできます。参照: https://sqlite.org/lang_conflict.html .
削除トリガーに関する文にも注意してください。REPLACE競合解決戦略が制約を満たすために行を削除する場合、再帰トリガーが有効な場合にのみ削除トリガーが起動します。
行を削除する余裕がない場合でも、changes()=0
とINSERT OR IGNORE
の両方を避けたい場合-このロジックを使用できます。
最初に挿入(存在しない場合)、次に更新一意のキーでフィルタリングします。
-- 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';
注意:どのトリガーが呼び出されているかを確認するためにテストしていませんが、私はassume次のようにします:
このようにして、単一の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);
編集:オプション2を追加しました。