SQLite 外键约束开了却没生效
建表语句里写了 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; 放进连接初始化里。外键没生效,多数时候不是表建错了,而是开关根本没开,或者开在了另一个连接上。
- 点赞
- 收藏
- 关注作者
评论(0)