三类完整性落到四种约束上:主键管唯一、外键管引用、CHECK 管取值、DEFAULT 补缺。
沙箱的外键默认是关的,插一条「父表里没有的 id」照样成功。
约束挡在写入那一刻,应用层能绕过校验,约束绕不过。
13.1 讲三类完整性:实体、参照、用户定义。这一篇把它们落到四种具体约束上,每种都撞一次墙。
第 6 章讲建表时写过 PRIMARY KEY 和 NOT NULL,那时只知道「写了就行」。
这一篇补上「不写会怎样、写了报什么错」。
第 13 章三篇:这一篇讲有哪几种约束,13.2 讲给约束起名,13.3 讲怎么改怎么删。
一、三类完整性,四种约束
教材按「管什么」把完整性分成三类,落到建表语句里就是四种约束。
| 完整性 | 靠什么保证 |
|---|---|
| 实体完整性 | PRIMARY KEY/UNIQUE/NOT NULL |
| 参照完整性 | FOREIGN KEY |
| 用户定义完整性 | CHECK/DEFAULT/NOT NULL |
「实体完整性」这个说法听着抽象,其实就一句话:这一行得能被唯一认出来。
主键干这个,唯一约束是它的补充——一张表只能有一个主键,但可以有多个唯一约束。
第三类「用户定义」就是业务规则:价格不能为负、状态只能是几个固定值。
这类规则原本写在应用代码里,写进 CHECK 之后就由数据库兜底了。
二、实体完整性:主键与唯一
插一条重号的主键,报的是 UNIQUE constraint failed: tb_cat.id。
但插一条主键为 NULL 的记录,居然成功了。
| 动作 | 结果 |
|---|---|
| 主键重复 | UNIQUE constraint failed |
主键为 NULL |
成功,自动编号 |
原因在 INTEGER PRIMARY KEY 身上:它就是 rowid,而 rowid 遇到 NULL 会「替你编一个」。
所以「主键不许为空」这句话要打个折——它不许的是重复。
想真的不许空,得在列上加 NOT NULL,光写主键不够。
这是一处很小的分叉,但真会踩:以为主键就够,结果插进了一堆空值。
三、非空:NOT NULL
name 上有 NOT NULL,插空值报 NOT NULL constraint failed: tb_goods.name。
这句报错把表名和列名都写全了,比主键那句好用。
报错质量不一样,这件事在排错时很关键。
外键那句只有 FOREIGN KEY constraint failed,什么信息都没有——下一篇会专门说。
四、参照完整性:外键默认不管事
这是这一章最该记住的一条:外键写在建表语句里,但默认不生效。
| 开关 | 插父表里没有的 id | 行数 |
|---|---|---|
| 关(默认,读到 0) | 成功 | 2 → 3 |
开(PRAGMA foreign_keys=ON) |
报 FOREIGN KEY constraint failed |
不变 |
99 这个类别根本不存在,但它插进去了。
换一个连接进来,这个开关又回到关的状态——它是连接级的设置,不是库级的。
打开之后两种操作都被拦:插一条指向不存在父行的记录、删一条还被引用的父行。
第二种更值得注意:约束管的不只是新数据,也管你删旧数据。
五、用户定义完整性:CHECK 与 DEFAULT
CHECK 拦非法值,DEFAULT 补缺失值,两件事管的方向相反。
| 写法 | 管什么 |
|---|---|
CHECK (price >= 0) |
写进来的值必须合法 |
DEFAULT 0 |
没写的列自动补一个值 |
插 price = -5 被拦,报 CHECK constraint failed: ck_price。
不写 price 那一列没报错,它被补成了 0。
两个都写,才叫「这一列的值既合法又完整」。
只写 CHECK,空值不会被拦(NULL >= 0 是「不知道」,不是「假」)。
这一点容易忽略:CHECK 对 NULL 是放行的。
想禁止空值,得另外加 NOT NULL,两个约束各管一件事。
六、约束挡住之后,脏数据没进表
再插一条负价的记录,报错照旧,表里的行数一点没变。 约束是在写入那一瞬间拦住的,不存在「先写进去再回滚」。 所以它是最后一道防线:应用层校验可以跳过(换个客户端、跑个脚本),约束跳不过。 但这不代表应用层可以不校验——约束的报错对用户不友好,只能兜底。
七、外键的四个动作
外键后面能跟一个动作,决定「删父行时子表怎么办」。默认是 NO ACTION。
| 动作 | 删父行时子表怎么办 |
|---|---|
NO ACTION(默认) |
拒绝删,报外键错误 |
CASCADE |
子表跟着删掉 |
SET NULL |
子表外键列置 NULL |
RESTRICT |
同 NO ACTION,立刻检查 |
实测 CASCADE:父行删掉之后,两条子行跟着消失,子表从 3 行变 1 行。
NO ACTION 就是上一节看到的「直接拒绝」。
选哪个动作要按业务定:订单明细该 CASCADE,账目记录应该 NO ACTION。
选错了不会报错,是数据悄悄少了一批——这种事最难查。
八、动手 Demo:四种约束各撞一次墙
这一段建两张表,然后依次让主键、非空、外键、CHECK 各失败一次,把报错原文原样记下来。
第三节专门演示「外键默认关着」这件事。
import sqlite3
c = sqlite3.connect(u":memory:", isolation_level=None)
c.executescript(u"""
CREATE TABLE tb_cat (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE tb_goods (
id INTEGER PRIMARY KEY, name TEXT NOT NULL, cat_id INTEGER,
price REAL DEFAULT 0,
CONSTRAINT ck_price CHECK (price >= 0),
CONSTRAINT fk_cat FOREIGN KEY (cat_id) REFERENCES tb_cat (id));
INSERT INTO tb_cat VALUES (1, '五金'), (2, '劳保');
INSERT INTO tb_goods VALUES (1, '螺丝刀', 1, 25.0), (2, '手套', 2, 8.0);
""")
# ① 实体完整性
def try_(fn):
try:
fn()
return u"成功"
except sqlite3.Error as e:
return u"%s" % e
print(try_(lambda: c.execute(u"INSERT INTO tb_cat VALUES (1, '重号')")))
print(try_(lambda: c.execute(u"INSERT INTO tb_cat VALUES (NULL, '空号')")))
print(c.execute(u"SELECT * FROM tb_cat").fetchall())
# ② 非空
print(try_(lambda: c.execute(u"INSERT INTO tb_goods (id, name) "
u"VALUES (9, NULL)")))
# ③ 外键:先看开关,再看打开后的行为
print(c.execute(u"PRAGMA foreign_keys").fetchone()[0])
print(try_(lambda: c.execute(u"INSERT INTO tb_goods VALUES (3, '野', 99, 10)")))
c.execute(u"PRAGMA foreign_keys = ON")
print(try_(lambda: c.execute(u"INSERT INTO tb_goods VALUES (4, '野2', 99, 10)")))
print(try_(lambda: c.execute(u"DELETE FROM tb_cat WHERE id = 1")))
# ④ CHECK 与 DEFAULT
print(try_(lambda: c.execute(u"INSERT INTO tb_goods VALUES (5, '负价', 1, -5)")))
print(try_(lambda: c.execute(u"INSERT INTO tb_goods (id, name, cat_id) "
u"VALUES (6, '没写价', 1)")))
print(c.execute(u"SELECT price FROM tb_goods WHERE id = 6").fetchone())
实跑输出:
--------------------------------------------------------------------
第五十七课:三种完整性,四种约束,一个默认关着的开关
--------------------------------------------------------------------
**教材 13.1 讲三类完整性:实体、参照、用户定义。**
**这一课把它们落到四种具体约束上,每一种都撞一次墙。**
**撞墙比看定义有用:报错原文里写着约束的名字和列名,那才是能用来查错的东西。**
一、实体完整性:主键与唯一
--------------------------------------------------------------------
动作 结果
------------------------------------------------
主键重复 UNIQUE constraint failed: tb_cat.id
主键为 NULL 成功
**第一行报 `UNIQUE constraint failed: tb_cat.id`,第二行却成功了。**
**因为 `INTEGER PRIMARY KEY` 就是 rowid,把 NULL 当成「你替我编一个」,于是给了 3。**
mysql> SELECT * FROM tb_cat
id name
-------
1 五金
2 劳保
3 空号
3 rows in set
**看结果那第三行:它拿到的是 3,不是空。**
**所以「主键不许为空」这句话在沙箱里要打个折——它不许的是「重复」。**
**想真的不许空,得给列上加 `NOT NULL`,光写主键不够。**
二、非空:NOT NULL
--------------------------------------------------------------------
**`name` 上有 `NOT NULL`,插空值报的是 `NOT NULL constraint failed: tb_goods.name`。**
**这句报错把「哪张表哪一列」都写全了,比主键那句更好用。**
**报错质量不一样,这件事在排错时很关键。**
三、参照完整性:外键默认不管事
--------------------------------------------------------------------
**先看开关:`PRAGMA foreign_keys` 给 0,也就是关着。**
开关 插父表里没有的 cat_id 表里行数
-------------------------------------------
关(默认) 成功 2 → 3
**`99` 这个类别根本不存在,但它插进去了,行数从 2 变 3。**
**外键写在建表语句里,默认却是不生效的——这一条最容易被忽略。**
开关 动作 结果
--------------------------------------------------------
开 插父表没有的 cat_id FOREIGN KEY constraint failed
开 删还被引用的父行 FOREIGN KEY constraint failed
**打开之后两边都被拦住了,报的是同一句 `FOREIGN KEY constraint failed`。**
**注意这句报错没说是哪张表、哪一列——这就是外键报错的短板,后面一篇还会提。**
四、用户定义完整性:CHECK 与 DEFAULT
--------------------------------------------------------------------
动作 结果
-----------------------------------------------
`price = -5` CHECK constraint failed: ck_price
不写 `price` 成功
mysql> SELECT id, name, price FROM tb_goods WHERE id = 6
id name price
---------------
6 没写价 0
1 row in set
**第一行被 `CHECK (price >= 0)` 拦了,报错里带着条件表达式。**
**第二行没报错,`DEFAULT 0` 把价格补成了 0。**
**`CHECK` 和 `DEFAULT` 是两件事:前者拦非法值,后者补缺失值。**
**两个都写,才叫「这一列的值既合法又完整」。**
五、约束挡住之后,脏数据没进表
--------------------------------------------------------------------
时机 行数
----------------
插负价之前 4
插负价之后 4
**报错是 `CHECK constraint failed: ck_price`,行数一点没变。**
**约束是在写入的那一瞬间拦住的,不存在「先写进去再回滚」这个过程。**
**所以它是最后一道防线,比应用层校验更可靠——应用能绕过,约束绕不过。**
六、外键的三个动作
--------------------------------------------------------------------
动作 子表行数
-------------------------------
删父行之前 3
删父行之后(CASCADE) 1
mysql> SELECT * FROM tb_c
id pid
------
3 2
1 row in set
动作 删父行时子表怎么办
-------------------------------------------
`NO ACTION`(默认) 拒绝删,报外键错误
`CASCADE` 子表跟着删掉
`SET NULL` 子表外键列置 NULL
`RESTRICT` 同 NO ACTION,立刻检查
**这次是 `CASCADE`:父行删掉,两条子行跟着没了,只剩 `pid = 2` 那条。**
**默认的 `NO ACTION` 就是上一节看到的那种:直接拒绝。**
**选哪个动作要按业务定:订单明细该 `CASCADE`,账目记录应该 `NO ACTION`。**
**选错了不是报错,是数据悄悄少了一批——这种事最难查。**
第一组两个结果很值得对照:第一行报 UNIQUE,第二行给「成功」。
紧接着打出的表里能看到那一行拿到了 3——NULL 被理解成「自动编号」。
第三组三行是这一篇的核心:开关读到 0,接着插一条野数据成功了;
打开开关之后,同样的插入被拦,删被引用的父行也被拦。
第四组三行:负价被 CHECK 拦,不写价格那一行成功且被补成 0。
CHECK 管合法,DEFAULT 管完整,这条分工在这里看得很清楚。
最后一段是 CASCADE 的实测:父行删掉,两条子行跟着没了,子表只剩 1 行。
换成默认的 NO ACTION,这一步会直接报错。
30 秒实验
- 把
id INTEGER PRIMARY KEY改成id INTEGER PRIMARY KEY NOT NULL,再插一次空值。 - 把
ck_price的条件改成price > 0,试插price = 0,看拦不拦。 - 把外键的
ON DELETE CASCADE去掉,重跑删父行那一步,看报什么错。
常见误区
- 以为写了外键就有人在管,其实沙箱默认不检查,脏数据一路插到底。
- 以为主键就一定不许为空,
INTEGER PRIMARY KEY遇到NULL会替你编号。 - 把
CHECK (price >= 0)当成「不许为空」,空值其实是放行的,得另加NOT NULL。
自测三问(点开对答案)
1. 为什么沙箱里插一条父表里没有的外键值会成功? —— 因为 `PRAGMA foreign_keys` 默认是 0,也就是关着。 它是连接级设置,每换一个连接都得自己打开,外键才真的开始检查。 2. `CHECK` 和 `DEFAULT` 各管什么? —— `CHECK` 拦非法值(写进来的必须合法),`DEFAULT` 补缺失值(没写的列自动填)。 注意 `CHECK` 对 `NULL` 是放行的,想禁止空值得另加 `NOT NULL`。 3. 外键的 `CASCADE` 和 `NO ACTION` 差在哪? —— `NO ACTION` 是拒绝删父行;`CASCADE` 是删父行时把对应的子行一起删掉。 选错不会报错,只会让数据悄悄少一批。约束定义会了,下一篇讲怎么给它们起名字——不起名,将来就删不掉。
- ☐ (本篇完,下一篇 13.2 命名完整性约束)