SQLite INSERT OR REPLACE 后关联子表数据被级联删除
唯一键冲突的 REPLACE 会删除旧父行,延迟外键检查不能保住子记录;改用保留父键的 UPSERT,并单独核对已提交的数据损失。
把外部数据同步到 SQLite 父表,使用 INSERT OR REPLACE 后,父表里仍能看到同一个业务编号,关联的子表备注却消失了。把外键改成 DEFERRABLE INITIALLY DEFERRED,或在外面包一层事务,也没有保住这些备注。
当写入违反 UNIQUE 或主键约束时,REPLACE 会先删除造成冲突的已有行,再完成插入。若被删父行关联着 ON DELETE CASCADE 子表,级联删除就有了触发条件;之后插回相同编号,不会把已删子行重新生成。
本文限定启用了外键支持和外键检查的普通 SQLite 表。下文修复采用 SQLite 3.24.0 起提供的 UPSERT,且目标是更新同一业务记录、保留父键及子表数据。
确认冲突键和真实写入语句
先暂停可能重复删除数据的同步任务,保留数据库备份、输入批次和应用日志。不要为了复现直接在生产库再执行一次替换。
在应用实际使用的连接中查看版本与外键设置:
SELECT sqlite_version();
PRAGMA foreign_keys;
PRAGMA foreign_key_list('extra_data');
extra_data 是子表名示例。检查其引用的父表、父键以及 on_delete 动作,再核对父表的主键和唯一索引。只有写入撞上相应唯一性约束,REPLACE 才会走这里讨论的删除路径;没有冲突的新行插入,不能据此解释子表丢失。
同时检查 ORM 或导入工具最终发出的 SQL。应用层方法叫“保存”或“更新”,并不证明底层用了 UPDATE。如果实际是 INSERT OR REPLACE,应按它的冲突行为评估。
保留父记录,改为冲突时更新字段
下面是一组只供空白隔离测试库使用的 SQL。它保留原有的 ON DELETE CASCADE,用真正的更新分支处理同一个 remote_id;最后回滚全部样例变更:
PRAGMA foreign_keys = ON;
BEGIN;
CREATE TABLE base_data (
local_id INTEGER PRIMARY KEY,
remote_id INTEGER NOT NULL UNIQUE,
title TEXT NOT NULL
);
CREATE TABLE extra_data (
remote_id INTEGER PRIMARY KEY
REFERENCES base_data(remote_id) ON DELETE CASCADE,
note TEXT NOT NULL
);
INSERT INTO base_data(local_id, remote_id, title)
VALUES (10, 42, 'old title');
INSERT INTO extra_data(remote_id, note)
VALUES (42, 'keep this note');
INSERT INTO base_data(remote_id, title)
VALUES (42, 'new title')
ON CONFLICT(remote_id) DO UPDATE SET title = excluded.title;
SELECT local_id, remote_id, title FROM base_data;
SELECT remote_id, note FROM extra_data;
ROLLBACK;
预期父行仍为 local_id = 10、remote_id = 42,标题更新,备注仍在。excluded.title 表示这次尝试插入的标题;更新列表没有改变父键,也没有删除父行。若中途出错,先处理该测试事务的回滚,不继续把失败后的查询输出当成成功结果。
接入应用时,用驱动参数绑定业务值,保持真实的冲突键和更新字段。冲突目标必须对应已有的唯一性约束,不能仅因为字段叫编号就假定它唯一。若同时有多个独立唯一约束,需明确每种冲突如何处理;本例没有承诺一条 ON CONFLICT(remote_id) 能接管其他唯一键的冲突。
UPSERT 也不是忽略所有错误。更新值违反 NOT NULL、CHECK、外键或其他约束时仍可能失败,应保留正常错误处理。更新触发器仍可能执行;如果触发器自身删除子行,单独改掉 REPLACE 并不能消除那条业务逻辑。
延后检查不会撤销级联动作
延迟外键约束控制的是“何时报告约束不满足”,不是把父表的删除当成暂存状态、等待随后插入再取消。ON DELETE CASCADE 是引用动作,删除父行时会处理相关子行。
把替换包进事务,可以提供整体提交或回滚的边界,但事务提交前若未主动回滚,删除与重新插入都属于这次事务的修改。它不会自动把这一组操作等价成 UPDATE。
也不要依靠 recursive_triggers 关闭级联行为。REPLACE 对用户定义的 DELETE 触发器有特定规则,这不能当成外键级联开关。为了导入临时关闭外键,则会改变完整性保障;外键设置在事务中的调整还可能无效,不应作为保留备注的快捷修复。
旧于 3.24.0 的应用运行库不能使用上述 UPSERT。优先评估驱动或嵌入库升级;若必须保留旧版,需要另行设计带并发保护的更新/插入流程,不能把无锁的“先查有没有,再插入”当成等价替代。
修好写入方式后,单独恢复已丢数据
用已有业务键、新业务键和其他唯一键冲突分别验收,比较父表身份、字段值和子表数量,并检查真正删除父行时原有级联语义仍符合要求。不要只确认父表标题已经更新。
已经提交的级联删除不会因恢复旧 SQL 或部署 UPSERT 自动回滚。需要从已验证的备份或其他可靠来源核对子表内容,再按恢复流程补回;仍在未提交事务里的错误写入,应由事务拥有者决定回滚。保留原输入和变更前数据,避免后续重跑掩盖最初丢失范围。


