SQLite 外键约束开了却没生效

举报
yd_290897247 发表于 2026/11/05 09:28:26 2026/11/05
【摘要】 建表语句里写了 FOREIGN KEY,插入一条明显违规的数据,SQLite 却一声不吭收下了。删父表记录,子表孤儿数据还安稳躺着。约束写在表结构里,却像不存在一样。这个问题不挑版本,新手老手都踩过,根源不在建表语句,而在连接上。 现象:建表写了外键,违规数据照样入库复现用内存库就行,不用建文件:PRAGMA foreign_keys;CREATE TABLE author(id INTEG...

建表语句里写了 FOREIGN KEY,插入一条明显违规的数据,SQLite 却一声不吭收下了。删父表记录,子表孤儿数据还安稳躺着。约束写在表结构里,却像不存在一样。这个问题不挑版本,新手老手都踩过,根源不在建表语句,而在连接上。

现象:建表写了外键,违规数据照样入库

复现用内存库就行,不用建文件:

PRAGMA foreign_keys;
CREATE TABLE author(id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE book(
  id INTEGER PRIMARY KEY,
  title TEXT,
  author_id INTEGER REFERENCES author(id)
);
INSERT INTO author VALUES(1, 'a');
INSERT INTO book VALUES(1, 'x', 999);
SELECT * FROM book;

输出:

0
1|x|999

author 表里根本没有 id=999 的作者,book 却成功插入了 author_id=999。再试删除父表记录:

DELETE FROM author WHERE id = 1;
SELECT * FROM author;
SELECT * FROM book;

author 空了,book 还在。外键约束全程没有拦过一次。表结构用 .schema 查,REFERENCES 明明白白写着,约束就是没起作用。

判据:先看 foreign_keys,再看是不是同一个连接

定位分两步。第一步看开关状态:

PRAGMA foreign_keys;

返回 0,说明当前连接根本没开外键。第二步确认操作是不是发生在同一个连接里。SQLite 的外键开关是连接级的,不是库级、不是表级、更不是全局配置。A 连接开了,B 连接照样是关的。换个连接查一次 PRAGMA foreign_keys;,结果经常不一样,这就是很多人“明明开了却没生效”的现场。

还有一个常见误判:以为建表时写 REFERENCES 就等于开了约束。不是。REFERENCES 只是声明规则,执行规则需要开关打开。开关关着,声明就是摆设。反过来也一样,开关开了但建表没写 REFERENCES,同样不会凭空多出约束。

根因:默认关闭,而且每个连接都要单独开

SQLite 出于历史兼容原因,把 foreign_keys 的默认值设为 OFF。文档里写得很直接:为了兼容老应用,默认不强制外键。每个新连接进来都是 OFF 状态,除非应用主动打开。

这个设计带来三个坑:

其一,命令行和程序行为不一致。sqlite3 命令行里手动 PRAGMA foreign_keys = ON; 再操作,看着一切正常;程序里重新打开一个连接,没写这句,约束又没了。测试时通过,上线后失效,大多是这个原因。

其二,连接池把问题藏得更深。池子里每个连接状态独立,有的开了有的没开。同一段插入代码,有时被拦有时放行,表现像随机故障,查代码查不出问题,因为代码本身没错,错在连接初始化没统一。

其三,开关不能在事务中间改。官方规则是 PRAGMA foreign_keys 不能在事务内切换。已经 BEGIN 之后才想起来开,会直接被拒绝,或者表现不符合预期。正确做法是在连接建立后、事务开始前就打开。

修法:连接建立后立刻打开,并且让它对每个连接都生效

最小修复只有一行,放在连接建立之后、任何建表和写入之前:

PRAGMA foreign_keys = ON;

验证开关确实打开:

PRAGMA foreign_keys;

返回 1 才算数。返回 0 就别往下写数据,先排查是不是在事务里、是不是换了连接。

程序里的写法要落在连接初始化的位置,不是业务代码里随手补一句。以 Python 为例:

import sqlite3
con = sqlite3.connect(":memory:")
con.execute("PRAGMA foreign_keys = ON")
print(con.execute("PRAGMA foreign_keys").fetchone())

输出 (1,) 才算打开。注意 connect 之后立刻执行,不要等到第一次写入前才想起来。如果用连接池,初始化函数里统一加这一句,让池里每个连接都带上开关,而不是依赖某次手工操作。

还有一处容易漏:级联动作也要在建表时写清楚。只写 REFERENCES,删除父表记录时默认动作是 NO ACTION,不会自动删子表。想要删作者连带处理书记录,得显式写:

CREATE TABLE book(
  id INTEGER PRIMARY KEY,
  title TEXT,
  author_id INTEGER REFERENCES author(id) ON DELETE CASCADE
);

ON DELETE CASCADE、ON DELETE SET NULL、ON DELETE RESTRICT 各有用途,选哪个看业务语义,不能指望默认值替你做决定。

验证:同一批操作重跑一遍,违规写入必须被拦下

开关打开后,把现象那段操作原样重跑:

PRAGMA foreign_keys = ON;
CREATE TABLE author(id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE book(
  id INTEGER PRIMARY KEY,
  title TEXT,
  author_id INTEGER REFERENCES author(id)
);
INSERT INTO author VALUES(1, 'a');
INSERT INTO book VALUES(1, 'x', 999);

这次输出应该是报错,而不是一行新记录:

Runtime error: FOREIGN KEY constraint failed

再验证删除保护:

INSERT INTO book VALUES(1, 'x', 1);
DELETE FROM author WHERE id = 1;

同样应该报错:

Runtime error: FOREIGN KEY constraint failed

因为 book 还引用着 author 的 id=1,父表记录删不掉。两处都拦住了,说明约束真正生效了。最后补一个正向用例,确认正常数据不受影响:

INSERT INTO book VALUES(2, 'y', 1);
SELECT * FROM book;

输出:

1|x|1
2|y|1

合法写入照常通过,违规写入被拦下,这个状态才是修好的标志。

回头总结这条排查链:先查 PRAGMA foreign_keys; 是不是 1,再确认操作和开关是不是同一个连接,最后把 PRAGMA foreign_keys = ON; 放进连接初始化里。外键没生效,多数时候不是表建错了,而是开关根本没开,或者开在了另一个连接上。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。