SQLite UPSERT/UPDATE或INSERT

bgu*_*ach 92 database sqlite upsert

我需要对SQLite数据库执行UPSERT/INSERT或UPDATE.

在许多情况下,命令INSERT OR REPLACE可能很有用.但是如果你想因为外键而使你的id保持自动增量,那么它不起作用,因为它删除了行,创建了一个新行,因此这个新行有一个新的ID.

这将是表格:

播放器 - (id上的主键,user_name唯一)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |
Run Code Online (Sandbox Code Playgroud)

bgu*_*ach 104

问答风格

那么,研究和与时间问题战斗后,我发现有两种方法可以做到这一点,取决于你的表的结构,如果你有激活保持正直外键约束.我想以干净的格式分享这个,以节省一些可能在我的情况下的人.


选项1:您可以负担得起删除该行

换句话说,您没有外键,或者如果您有外键,则配置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)
Run Code Online (Sandbox Code Playgroud)

诀窍是合并.它返回用户'steven'的id(如果有的话),否则返回一个新的id.


选项2:您无法承担删除该行的费用

在使用之前的解决方案进行调整之后,我意识到在我的情况下最终可能会破坏数据,因为此ID可用作其他表的外键.此外,我使用ON DELETE CASCADE子句创建了表,这意味着它将以静默方式删除数据.危险的.

所以,我首先想到了一个IF子句,但SQLite只有CASE.如果EXISTS(从user_name ='steven'的玩家中选择id ),则不能使用此CASE(或者至少我没有管理它)执行一个UPDATE查询,如果没有则INSERT.不行.

然后,最后我使用了蛮力,并取得了成功.逻辑是,对于您要执行的每个UPSERT,首先执行INSERT或IGNORE以确保我们的用户有一行,然后执行UPDATE查询,其中包含您尝试插入的完全相同的数据.

与以前相同的数据:user_name ='steven',年龄= 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'; 
Run Code Online (Sandbox Code Playgroud)

就这样!

编辑

正如Andy所评论的那样,尝试先插入然后更新可能会导致触发触发的频率高于预期.这不是我认为的数据安全问题,但是发起不必要的事件确实没有意义.因此,改进的解决方案是:

-- 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); 
Run Code Online (Sandbox Code Playgroud)

  • 同上......选项2很棒.除此之外,我反过来做了:尝试更新,检查rowsAffected是否> 0,如果没有,则执行插入. (10认同)
  • 您不需要在上一个代码示例的update语句中重新设置user_name。设定年龄就足够了。 (2认同)

Mar*_*eIV 67

这是一种不需要暴力"忽略"的方法,只有在存在密钥违规的情况下才会有效.这种方式基于您在更新中指定的任何条件.

试试这个...

-- 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);
Run Code Online (Sandbox Code Playgroud)

这个怎么运作

这里的"魔术"是你使用该Where (Select Changes() = 0)子句来确定插入是否有任何行,并且由于这是基于你自己的Where子句,它可以是你定义的任何东西,而不仅仅是密钥违规.

在上面的示例中,如果更新没有更改(即记录不存在),则Changes()= 0,因此Where语句中的子句Insert返回true,并插入带有指定数据的新行.

如果Update 确实更新了现有行,那么Changes()= 1,因此现在的'Where'子句Insert将为false,因此不会发生插入.

不需要蛮力.


pra*_*pin 42

这是一个迟到的答案.从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;
Run Code Online (Sandbox Code Playgroud)

  • 为什么我不能在安卓上使用这个?我尝试了 `db.execSQL("在冲突(id) 上插入 bla(id,name) 值 (?,?) 执行更新集 name=?")`。给我一个关于“on”这个词的语法错误 (2认同)
  • @BastianVoigt 因为安装在各种 Android 版本上的 SQLite3 库早于 3.24.0。请参阅:https://developer.android.com/reference/android/database/sqlite/package-summary 遗憾的是,您需要在 Android 或 iOS 上使用 SQLite3(或任何其他系统库)的新功能,您需要捆绑一个SQLite 在您的应用程序中的特定版本,而不是依赖于系统安装的一个。 (2认同)
  • 与 UPSERT 相比,这不是更像 INDATE 因为它首先尝试插入吗?;) (2认同)

And*_*ndy 25

所有提出的答案的问题完全没有考虑到触发器(可能还有其他副作用).解决方案如

INSERT OR IGNORE ...
UPDATE ...
Run Code Online (Sandbox Code Playgroud)

当行不存在时,导致执行两个触发器(用于插入,然后用于更新).

适当的解决方案是

UPDATE OR IGNORE ...
INSERT OR IGNORE ...
Run Code Online (Sandbox Code Playgroud)

在这种情况下,只执行一个语句(当行存在或不存在时).

  • 可读性?我一眼就能看出安迪的代码在做什么。你的 bgusach 我必须研究一分钟才能弄清楚。 (2认同)

Gil*_*lco 5

拥有一个不带唯一键和其他键的无孔纯正UPSERT(对于程序员):

UPDATE players SET user_name="gil", age=32 WHERE user_name='george'; 
SELECT changes();
Run Code Online (Sandbox Code Playgroud)

SELECT changes()将返回上次查询中完成的更新次数。然后检查changes()的返回值是否为0,如果是,则执行:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 
Run Code Online (Sandbox Code Playgroud)


its*_*sho 5

选项 1:插入 -> 更新

如果您想避免两者changes()=0INSERT 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';
Run Code Online (Sandbox Code Playgroud)

关于触发器

注意:我还没有测试它来查看正在调用哪些触发器,但我假设如下:

如果行不存在

  • 插入前
  • 使用 INSTEAD OF 插入
  • 插入后
  • 更新前
  • 使用 INSTEAD OF 更新
  • 更新后

如果行确实存在

  • 更新前
  • 使用 INSTEAD OF 更新
  • 更新后

选项 2:插入或替换 - 保留您自己的 ID

这样你就可以有一个单一的 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);
Run Code Online (Sandbox Code Playgroud)

编辑:添加选项 2。