Skip to content

第 6 章 关联表的定义 ​

从一个生活场景说起 ​

现实世界里的信息从来不是孤立的。想想外卖订单:

  • 一个用户可以下很多订单;
  • 每张订单都必须属于某一个用户。

如果让你用 Excel 管理,你会怎么做?最笨的办法是把用户的姓名、电话、地址重复抄到每张订单上——用户改了手机号,你得改几十张订单,抄漏一处数据就乱了。

聪明的做法是:建一张"用户表"给每个用户编个号,订单表上只写一句"这单是 3 号用户的"。这个"3 号"就是外键——它像一张指向另一张表的字条,让两张表产生了关联。

术语解释 —— 外键(Foreign Key):一张表中的某一列,它的值引用另一张表的主键。比如订单表的 author_id 列存放用户表的 id 值,就把订单和用户关联了起来。

核心概念 ​

一对多关系 ​

"一个用户有多篇文章,一篇文章只属于一个用户"——这叫一对多(One-to-Many):

  • "一"方(user):主键被别人引用;
  • "多"方(article):持有外键,记录"我属于谁"。

规律:外键永远放在"多"的一方,就像每个孩子的出生证上写着父母的编号,而不是父母的证件上写满所有孩子的编号。

删除主表记录时,从表怎么办? ​

如果删掉一个用户,他的文章该怎么办?这就是级联策略(ondelete),三种常见选择:

策略含义生活类比
CASCADE连带删除:删用户时他的文章一起删注销账号,历史动态一并清空
SET NULL置空:文章保留,但"作者"一栏清空员工离职,他负责的项目"负责人"一栏留白
默认 RESTRICT禁止删除:只要还有文章就不让删用户名下还有房产,不允许销户

示例代码(逐行讲解) ​

python
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, ForeignKey

engine = create_engine(
    "mysql+pymysql://root:123456@localhost:3306/demo_db?charset=utf8mb4",
    echo=True,
)
metadata = MetaData()

user_table = Table(
    "user", metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("name", String(50), nullable=False),
)

article_table = Table(
    "article", metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("title", String(200), nullable=False),
    # 外键:引用 user 表的 id;ondelete 定义删除用户时文章如何处理
    Column("author_id", Integer, ForeignKey("user.id", ondelete="CASCADE")),
)

metadata.create_all(engine)

核心就一行:

python
Column("author_id", Integer, ForeignKey("user.id", ondelete="CASCADE"))

它做了三件事:

  1. 在 article 表里加一列 author_id;
  2. 声明它的值必须来自 user 表的 id 列(ForeignKey("user.id"));
  3. 约定删除用户时,他的文章连带删除(ondelete="CASCADE")。

注意 ForeignKey 里写的是数据库的表名.列名(字符串 "user.id"),不是 Python 变量名。

关键知识点 ​

概念说明
外键ForeignKey("表名.列名"),建立表间的引用关系
一对多"一"方(user)的主键被"多"方(article)引用
ondelete级联策略:CASCADE(连带删除)/ SET NULL(置空)/ 默认 RESTRICT(禁止删除)

深入理解:外键是如何"管闲事"的? ​

外键不只是个标记,MySQL 的 InnoDB 引擎会强制执行它:

  • 往 article 插入 author_id=999,但 user 表里没有 id 为 999 的用户 → 拒绝插入;
  • 删除 id=1 的用户,而他还有文章(且没配 CASCADE)→ 拒绝删除;
  • 删除被引用的表 → 拒绝执行。

这就是"参照完整性":数据库替你保证,外键指向的目标永远真实存在,不会出现"查无此人"的幽灵数据。

进阶内容 ​

1. 删表的顺序问题。 因为有外键约束,删除表的顺序必须是:先删"多"方(article),再删"一"方(user)。metadata.drop_all(engine) 会自动按依赖关系排好序,不用你操心。

2. ER 图:动笔前先画图。 ER 图(实体-关系图)是数据库设计的草图:方框代表表,连线代表关系,线上标注 1 和 N(多)。哪怕只画在纸上,也能帮你提前发现设计缺陷。

3. 互联网大厂为什么 sometimes 不用外键? 高并发场景下,外键检查会带来额外锁开销,一些团队选择在应用层代码里保证一致性。这是"性能与严格性的取舍",但学习阶段请务必使用外键——先学会规则,再谈打破规则。

4. 外键与索引。 MySQL 会为外键列自动创建索引(没有索引外键无法工作),所以按 author_id 查某用户的所有文章是很快的。

注意事项 ​

  • MySQL 的 InnoDB 引擎默认强制检查外键:存在外键引用时,无法直接删除被引用的行,也无法随意删除被引用的表。这与 SQLite 默认不检查外键的行为不同。
  • 删除表的顺序有讲究:先删有外键的表(article),再删被引用的表(user);metadata.drop_all() 会自动按依赖顺序处理。
  • 外键列的类型必须与主键列一致(通常都是 Integer),否则建表会报错。
  • 外键会自动创建索引,无需手动为外键列再加索引。
  • 高并发互联网项目有时会放弃数据库外键、改由应用层保证一致性,但学习阶段建议使用外键理解数据约束。
  • 外键设计决定数据一致性,动手写代码前先画好 ER 图。

本章小结 ​

  • 外键 = 指向另一张表主键的"字条",放在"多"的一方;
  • 一对多关系:一个用户 ↔ 多篇文章;
  • ondelete 决定删主表记录时从表的命运:CASCADE / SET NULL / RESTRICT;
  • InnoDB 强制执行外键约束,保证"查无此人"的数据不会出现。