DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

SQLite 已设置 foreign_keys,删除父记录仍未触发级联删除

事务内的 PRAGMA foreign_keys 设置不会生效。核对实际写入连接的开关和外键定义,将初始化移到事务开始前,并单独检查历史孤儿记录。

SQLite 的子表已经声明 ON DELETE CASCADE,删除父表记录后,子表记录却还在。连接初始化代码里也写了 PRAGMA foreign_keys = ON,执行时没有报错。

如果调用顺序如下,外键开关可能根本没有打开:

BEGIN;
PRAGMA foreign_keys = ON;

SQLite 在事务进行期间不会改变这个开关,尝试设置也不会报错。如果这条连接原先是关闭状态,它仍然关闭。这个规则也适用于尚未结束的 SAVEPOINT。本文针对支持外键的 SQLite 3.6.19 及以后版本;应用实际链接的 SQLite 库需要保留外键和触发器支持。

在执行删除的那条连接上读取状态

不要只在数据库管理工具里检查。管理工具与应用通常使用不同连接,前者开启外键不能证明后者也开启。让应用在将要执行写入的同一条连接上读取:

PRAGMA foreign_keys;
PRAGMA foreign_key_list('child');

把 child 换成实际子表名称。第一个查询返回 1 才表示该连接正在执行外键约束,返回 0 表示关闭。第二个查询列出表上实际存在的外键,检查引用的父表、列及 on_delete 动作是否符合预期。代码里的建表语句不会证明现有数据库里的表结构相同。

如果第一个查询连一行 0 或 1 都没有,需检查实际 SQLite 库版本和编译选项;过旧版本或省略外键相关功能的构建不能靠调整语句顺序修好。如果返回 1,则不要继续把问题归因于开关未启用,应核对实际约束和删除条件。

将设置移到事务开始之前

在每条新连接完成建立、尚未交给业务代码开启事务时设置,并立即读取确认:

PRAGMA foreign_keys = ON;
PRAGMA foreign_keys;

应用应检查第二条语句返回的值,确认是 1 后再开始业务事务。若框架或驱动会自动开启事务,要把这两条语句放入其允许在事务外执行的连接初始化阶段。仅把 SQL 移到源码前几行,还不足以证明执行时没有活动事务。

使用连接池时,对每条新建的底层连接初始化。连接断开重建后也要重新设置;它不是保存到数据库文件、以后所有连接都会继承的开关。不要依赖某个环境的默认值,默认值可能受编译配置影响。

已经处于业务事务中时,先由原业务流程决定提交还是回滚,再调整初始化代码。不要为了让 PRAGMA 生效直接加一条 COMMIT,那会同时提交尚未审查的业务修改;盲目 ROLLBACK 也会丢弃本事务的改动。

用独立临时数据库验证级联行为

下面的 SQL 只用于新建、可丢弃的测试数据库,不要粘贴到已有业务库。每条语句应按顺序执行;客户端遇到错误时应停止,不能把后面的计数当作成功证明。

PRAGMA foreign_keys = ON;
PRAGMA foreign_keys;

BEGIN;
CREATE TABLE parent (
    id INTEGER PRIMARY KEY
);
CREATE TABLE child (
    id INTEGER PRIMARY KEY,
    parent_id INTEGER REFERENCES parent(id) ON DELETE CASCADE
);
INSERT INTO parent (id) VALUES (1);
INSERT INTO child (id, parent_id) VALUES (10, 1);
DELETE FROM parent WHERE id = 1;
SELECT COUNT(*) AS remaining_children FROM child;
ROLLBACK;

预期外键状态是 1,删除后的子表计数是 0。末尾回滚会撤销这个测试事务中的建表与数据修改。然后用应用实际使用的连接初始化路径,在隔离测试库做同样检查,避免只验证了命令行工具的连接。

正式启用前应保留可恢复的数据库备份,并检查受影响的写入流程。此前被放行的无效引用可能开始报 FOREIGN KEY constraint failed;有 CASCADE 的删除则会真正删除关联记录。不能把这两种行为变化都当成新故障而简单关闭约束。

开启开关不会补做过去的级联删除

外键关闭期间,如果父记录已经删除,重新开启开关不会自动清理留下的子记录。SQLite 3.7.16 起提供专门的检查命令,可检查现存违规数据:

PRAGMA foreign_key_check;

每条结果对应一项外键违规,包含子表、相关行的 rowid、父表和约束编号;WITHOUT ROWID 子表的第二列会是 NULL。约束编号可与 PRAGMA foreign_key_list('child') 的编号对照。在支持该命令的版本中,没有结果表示这次检查没有发现外键违规,不等于已验证全部业务规则。更旧版本遇到不认识的 PRAGMA 可能直接忽略,不能把这种无结果当作检查通过。

对历史孤儿记录,先确认应恢复父记录、修正引用还是删除子记录,保留备份并按业务规则处理。不要把检查结果直接拼成批量删除语句。需要回退应用初始化改动时,应同时评估是否会重新允许无效引用;恢复旧开关状态也无法撤销已经提交的级联删除。