SQLite UPSERT / 更新或插入
Posted
技术标签:
【中文标题】SQLite UPSERT / 更新或插入【英文标题】:SQLite UPSERT / UPDATE OR INSERT 【发布时间】:2013-02-23 00:23:30 【问题描述】:我需要对 SQLite 数据库执行 UPSERT / INSERT OR UPDATE。
命令 INSERT OR REPLACE 在很多情况下很有用。但是,如果您想因为外键而使您的 id 保持在适当的位置,则它不起作用,因为它会删除该行,创建一个新行,因此该新行具有一个新 ID。
这是一张桌子:
玩家 -(id 上的主键,用户名唯一)
| id | user_name | age |
------------------------------
| 1982 | johnny | 23 |
| 1983 | steven | 29 |
| 1984 | pepee | 40 |
【问题讨论】:
【参考方案1】:问答风格
好吧,在研究和解决这个问题几个小时后,我发现有两种方法可以实现这一点,具体取决于表的结构以及是否激活了外键限制以保持完整性。我想以简洁的格式分享此内容,以便为可能遇到我这种情况的人节省一些时间。
选项 1:您可以负担得起删除行
换句话说,您没有外键,或者如果您有外键,您的 SQLite 引擎已配置为不存在完整性异常。要走的路是插入或替换。如果您尝试插入/更新 ID 已存在的播放器,SQLite 引擎将删除该行并插入您提供的数据。现在问题来了:如何保持旧 ID 关联?
假设我们想要 UPSERT 使用数据 user_name='steven' 和 age=32。
看这段代码:
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。
选项 2:您不能删除该行
在尝试了之前的解决方案后,我意识到在我的情况下,这可能最终会破坏数据,因为此 ID 用作其他表的外键。此外,我使用子句 ON DELETE CASCADE 创建了表,这意味着它会静默删除数据。危险。
所以,我首先想到了一个IF子句,但是SQLite只有CASE。如果 EXISTS(select id from player where 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 age=32 WHERE user_name='steven';
-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32);
【讨论】:
同上...选项 2 很棒。除了,我做了相反的事情:尝试更新,检查 rowsAffected > 0,如果没有,则进行插入。 这也是一个很好的方法,唯一的小缺点是你没有只有一个 SQL 用于“upsert”。 您不需要在最后一个代码示例的更新语句中重新设置用户名。设置年龄就够了。【参考方案2】:这是一个迟到的答案。从 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,请参考下面的this answer(由我发布,@MarqueIV)。
但是,如果您确实可以选择升级,强烈建议您这样做,因为与我的解决方案不同,此处发布的解决方案在单个语句中实现了所需的行为。此外,您还可以获得更新版本通常附带的所有其他功能、改进和错误修复。
【讨论】:
目前,Ubuntu 存储库中还没有此版本。 为什么我不能在安卓上使用这个?我试过db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?")
。给我一个关于单词“on”的语法错误
@BastianVoigt 因为安装在各种 android 版本上的 SQLite3 库都早于 3.24.0。请参阅:developer.android.com/reference/android/database/sqlite/… 遗憾的是,如果您需要在 Android 或 ios 上使用 SQLite3(或任何其他系统库)的新功能,您需要在应用程序中捆绑特定版本的 SQLite,而不是依赖安装的系统。跨度>
而不是UPSERT,这不是更多的INDATE,因为它首先尝试插入? ;)
@BastianVoigt,请在下面查看我的答案(链接在上面的问题中),该答案适用于 3.24.0 之前的版本。【参考方案3】:
这是一种不需要蛮力“忽略”的方法,该方法仅在存在密钥违规时才有效。这种方式基于您在更新中指定的任何条件工作。
试试这个...
-- 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
子句中使用Changes()
。 Changes()
表示上次操作影响的行数,在本例中为更新。
在上面的示例中,如果更新没有更改(即记录不存在),则Changes()
= 0 因此Insert
语句中的Where
子句的计算结果为true,并且新行插入指定的数据。
如果Update
确实 更新了现有行,则Changes()
= 1(或更准确地说,如果更新了不止一行,则不为零),因此 'Where' 子句Insert
现在评估为 false,因此不会发生插入。
这样做的好处是不需要蛮力,也不需要删除,然后重新插入可能导致外键关系中下游键混乱的数据。
此外,由于它只是一个标准的Where
子句,它可以基于您定义的任何内容,而不仅仅是键违规。同样,您可以将Changes()
与您想要/需要的任何其他内容结合使用,只要允许使用表达式即可。
【讨论】:
这对我很有用。除了所有 INSERT OR REPLACE 示例之外,我还没有在其他任何地方看到此解决方案,它对我的用例来说更加灵活。 @MarqueIV 如果有两个项目必须更新或插入怎么办?例如,第一个已更新,而第二个不存在。在这种情况下Changes() = 0
将返回 false 并且两行将执行 INSERT OR REPLACE
通常一个 UPSERT 应该作用于一个记录。如果您说您确定它对多条记录起作用,则相应地更改计数检查。
不好的是,如果该行存在,则无论该行是否发生变化,都必须执行更新方法。
为什么这是一件坏事?如果数据没有改变,你为什么首先打电话给UPSERT
?但即便如此,更新发生是一件好的事情,设置Changes=1
否则INSERT
语句会错误地触发,这是你不希望的。【参考方案4】:
所有提供的答案的问题是完全没有考虑触发器(可能还有其他副作用)。 像这样的解决方案
INSERT OR IGNORE ...
UPDATE ...
当行不存在时,导致两个触发器都执行(用于插入,然后用于更新)。
正确的解决方案是
UPDATE OR IGNORE ...
INSERT OR IGNORE ...
在这种情况下,只执行一条语句(当行存在或不存在时)。
【讨论】:
我明白你的意思。我会更新我的问题。顺便说一句,我不知道为什么需要UPDATE OR IGNORE
,因为如果没有找到行,更新不会崩溃。
可读性?我一眼就能看出安迪的代码在做什么。你的 bgusach 我不得不研究一分钟才能弄清楚。【参考方案5】:
要有一个没有漏洞的纯 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);
【讨论】:
这相当于@fiznool 在他的评论中提出的建议(尽管我会寻求他的解决方案)。没关系,实际上工作正常,但是您没有唯一的 SQL 语句。不基于 PK 或其他唯一键的 UPSERT 对我来说几乎没有意义。【参考方案6】:您也可以在您的 user_name 唯一约束中添加一个 ON CONFLICT REPLACE 子句,然后直接插入,让 SQLite 找出在发生冲突时该怎么做。见:https://sqlite.org/lang_conflict.html。
还要注意关于删除触发器的句子:当 REPLACE 冲突解决策略删除行以满足约束时,删除触发器当且仅当启用递归触发器时才会触发。
【讨论】:
【参考方案7】:选项 1:插入 -> 更新
如果您想避免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';
关于触发器
注意:我尚未对其进行测试以查看正在调用哪些触发器,但我假设如下:
如果行不存在
插入前 使用 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);
编辑:添加选项 2。
【讨论】:
以上是关于SQLite UPSERT / 更新或插入的主要内容,如果未能解决你的问题,请参考以下文章
SQLCE - Upsert(更新或插入) - 如何使用常用方法准备一行?