一、外键的核心作用

核心目的:保证数据的“参照完整性”,防止出现孤儿数据。

  • 限制与校验:子表的关联字段必须在父表中存在(例如:不能给不存在的用户创建订单)。
  • 级联操作:父表数据变动时,子表自动跟着变(如:删用户,自动删订单)。
  • 建立关联:作为 ORM 定义模型关系(一对多/多对一)的基石。
    ⚠️ SQLite 巨坑提示:SQLite 默认不生效外键!必须在每次连接时手动开启:
1
2
3
4
5
6
from sqlalchemy import event
@event.listens_for(engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()

二、ForeignKey vs relationship(最容易混淆的点)

它们成对出现,但负责的层面完全不同:

ForeignKey relationship
作用层 🗄️数据库层面(SQL) 🐍Python 对象层面(ORM)
干了什么 告诉数据库:这个字段必须指向另一个表的主键 告诉 Python:可以通过属性直接访问关联对象
没有它会怎样 数据库不约束,可插入脏数据 只能用order.user_id,不能用 order.user
代码体验:
1
2
3
4
5
6
# ❌ 只有 ForeignKey:拿对象很麻烦
order = session.query(Order).first()
user = session.query(User).filter_by(id=order.user_id).first() # 手动查
# ✅ 配合 relationship:丝滑导航
order = session.query(Order).first()
print(order.user.name) # 直接拿到!ORM 自动帮你 JOIN

三、级联删除的双层防线(核心难点)

删除父表数据时,子表数据怎么办?ORM 和数据库各有一套机制:

① 数据库层级联(底线)

1
user_id = Column(Integer, ForeignKey('users.id', ondelete="CASCADE"))
  • 谁在干活? 数据库引擎(SQLite/MySQL)
  • 怎么干? 收到删除指令时,数据库底层直接用一条 SQL 搞定关联数据的删除。
  • 特点:极快(1条 SQL)、绕过 ORM 也生效,但 Python 层面的删除钩子函数不会触发。

② ORM 层级联(防线)

1
orders = relationship("Order", back_populates="user", cascade="all, delete-orphan")
  • 谁在干活? SQLAlchemy(Python 代码)
  • 怎么干? SQLAlchemy 先查出子表数据,在 Python 里逐个删除,最后再删父表。
  • 特点:能触发 Python 的钩子函数,但性能差(N+1 条 SQL),绕过 ORM 就失效。

四、灵魂拷问:两层都写,会不会删两遍冲突?

结论:不会冲突!
类比理解:

  • ORM 级联 = 你请的管家(跟着你时很靠谱)
  • 数据库级联 = 仓库的硬规矩(不管谁来都管用)
    当你通过 ORM 执行 session.delete(user) 时,执行顺序如下:
  1. 管家先动手:ORM 先查出关联订单,发 SQL 逐个删掉订单,最后删掉用户。
  2. 仓库空转:当 DELETE FROM users 发到数据库时,关联订单已经被 ORM 删光了。数据库的 ON DELETE CASCADE 想干活,但找不到关联数据,只能安全地空转一下。
    如果绕过 ORM 直接写 SQL 呢?
    管家(ORM)根本不知道你删了数据,这时候仓库的硬规矩(数据库级联)就起作用了,保证数据绝不留孤儿!

五、最佳实践口诀 🎯

1
2
3
4
5
6
7
8
9
10
11
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
# 防线:ORM 层级联,保证对象操作优雅且能触发钩子
orders = relationship("Order", back_populates="user", cascade="all, delete-orphan")
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
# 底线:数据库层级联,保证底层兜底绝对不留孤儿数据
user_id = Column(Integer, ForeignKey('users.id', ondelete="CASCADE"))
user = relationship("User", back_populates="orders")

ForeignKey 管数据库(地基),relationship 管 Python(盖楼)。
ORM cascade 是防线,DB ondelete 是底线,两层都写最稳妥!