Skip to content

第 16 章 Session 的使用与事务控制 ​

从一个生活场景说起 ​

这一章是全教程的"内功心法"。先用一个比喻把 Session 讲透。

Session 很像你在银行柜台办业务时遇到的那位柜员:

  1. 你取号坐下,柜员开始为你服务(创建 Session);
  2. 你说"先查一下余额",柜员帮你查(加载对象);
  3. 你说"取 500 块,再转 300 给张三"——柜员先记在工单上,不急着动账(add/修改对象,变更被跟踪);
  4. 你说"先打张回单我看看"——柜员把操作预演一遍给你看,账还没最终动(flush);
  5. 你确认无误说"就这样办"——柜员盖章,账目一次性生效(commit);
  6. 如果中途任何一步出问题(比如余额不足),柜员把整张工单撕掉,账目恢复原样(rollback);
  7. 你离开窗口,柜员接待下一位(close)。

关键认识:一个柜员一次只能服务一位客户。 如果两个客户同时指挥同一个柜员,工单必然记混——这就是"Session 不是线程安全的"。

核心概念 ​

Session 是 ORM 的核心,可以理解为"一次数据库会话":它跟踪你加载过的所有对象、记录所有变更,最后在 commit() 时统一写入数据库。

事务:同生共死的操作打包 ​

术语解释 —— 事务(Transaction):一组数据库操作的集合,具有"原子性"——要么全部成功,要么全部失败回滚。经典例子是转账:A 扣款和 B 入账必须绑定为一个整体,绝不能只完成一半。

Session 从创建起就自动开启一个事务,直到 commit() 或 rollback() 才结束。

事务控制示例(逐行讲解) ​

python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine(
    "mysql+pymysql://root:123456@localhost:3306/demo_db?charset=utf8mb4",
    echo=True,
)
# sessionmaker 是"柜员生产线":统一配置,每次调用生产一个新 Session
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)

with SessionLocal() as session:
    try:
        user = User(name="张三")
        session.add(user)
        session.flush()        # 预演:把 INSERT 发给数据库(但不提交),此时可拿到 user.id

        order = Order(user_id=user.id, amount=99.9)   # 用刚拿到的 id 建订单
        session.add(order)

        session.commit()       # 全部成功才提交:盖章生效
    except Exception:
        session.rollback()     # 任何一步失败,整体回滚:撕掉工单
        raise

这个例子的精妙之处在 flush():创建订单需要 user.id,但 id 要等 INSERT 执行后才由 MySQL 分配。flush() 就是"先把已有的变更发给数据库执行(事务还没结束),把 id 拿到手",然后继续后续操作。如果后面订单创建失败,rollback() 会把用户的插入也一并撤销——两者同生共死。

对象生命周期 ​

一个对象在 Session 中会经历四种状态:

临时态(Transient) → add() → 持久态(Pending) → flush → 托管态(Persistent)
                                                      ↓ commit 后
                                              游离态(Detached,session 关闭后)

用快递包裹来理解:

状态含义快递类比
Transient 临时态刚 new 出来,与数据库无关包裹还在你家里
Pending 持久态已 add,等待写入包裹已交给快递员,还没装车
Persistent 托管态已入库,被 Session 跟踪包裹在物流系统中,全程可查
Detached 游离态Session 已关闭包裹已签收,物流单号作废

第 11 章遇到的 DetachedInstanceError 就是"拿着作废的单号查物流":对象处于游离态时再触发懒加载,SQLAlchemy 找不到活的 Session 去查库,只能报错。

关键知识点 ​

方法作用
session.add()标记待插入
session.flush()发送 SQL 到数据库但不提交(事务内可见)
session.commit()提交事务
session.rollback()回滚事务,撤销未提交的变更
session.close()关闭会话,归还连接
sessionmakerSession 工厂,统一管理配置

深入理解:flush 和 commit 的区别 ​

这是新手最常混淆的一对:

flush()commit()
发 SQL 到数据库✅✅(内部先 flush)
结束事务❌ 事务还在✅ 事务结束
数据对其他连接可见❌ 仅本事务内可见✅ 全局可见
可以 rollback 撤销✅❌ 已生效

类比:flush 是"把草稿发给打印机预览",commit 是"正式打印并盖章下发"。

commit() 内部其实自动调用了 flush(),所以你平时只用 commit 就够了——只有当你需要提前拿到数据库生成的值(如自增 id)时,才显式调 flush。

深入理解:为什么不能共享 Session? ​

Session 内部维护着"哪些对象改了、改成什么样"的记账本。如果两个线程共用一个 Session:

  • 线程 A 刚 add(user),线程 B 就调了 commit()——A 的半成品数据被提前入库;
  • 两个线程的变更混在一本账上,谁 rollback 都会误伤对方。

铁律:每个线程/每个请求各自创建、使用、关闭自己的 Session。 sessionmaker 工厂的存在就是为了方便地"要一个,造一个"。

进阶内容 ​

1. MySQL 的事务隔离级别。 MySQL InnoDB 默认是 REPEATABLE READ(可重复读):一个事务内多次读同一行,结果都是事务开始时的快照。好处是读数据稳定一致;副作用是长事务看不到别人的新提交,而且快照会阻止 undo log 被清理,拖慢整个数据库。所以:事务要尽量短,别在事务里做网络请求、文件 IO 等慢操作。

2. 死锁与重试。 两个事务互相等待对方持有的锁,MySQL 会检测到死锁并强制回滚其中一个(报错 Deadlock found when trying to get lock)。健壮的应用层应该捕获这个错误并重试:

python
from sqlalchemy.exc import OperationalError

for attempt in range(3):
    try:
        with SessionLocal() as session:
            # ... 业务操作 ...
            session.commit()
        break
    except OperationalError as e:
        if "Deadlock" in str(e) and attempt < 2:
            continue        # 死锁了,重试
        raise

3. with SessionLocal() as session: 的隐藏福利。 退出 with 块时会自动 close();如果块内抛异常且你忘了 rollback,close() 会自动回滚未提交的变更。加上 try/except 双重保险,事务就很难"泄漏"。

注意事项 ​

  • 事务原则:一个业务流程要么全部成功要么全部失败,用 try/except + rollback 包裹。
  • Session 不是线程安全的,每个线程/请求应持有自己的 Session,绝不共享。
  • 推荐用 with Session() as s: 语法,退出时自动 close。
  • flush() 和 commit() 的区别:flush 只发 SQL 不结束事务,commit 发 SQL 并结束事务。
  • 长事务会占用连接和锁,事务要尽量短。MySQL InnoDB 中长事务会阻碍 undo log 清理、引发行锁等待甚至死锁。
  • MySQL 遇到死锁(Deadlock found when trying to get lock)时,事务会被自动回滚,应用层应捕获后重试。
  • 不要把耗时操作(调第三方 API、读文件)放在事务中间——先准备好数据,再开事务快速写入。

本章小结 ​

  • Session = 银行柜员:记账本、管事务、一人一位不共享;
  • 事务 = 同生共死的操作打包,commit 生效 / rollback 作废;
  • flush 是"预演",commit 是"盖章";需要提前拿 id 时用 flush;
  • 对象四状态:临时态 → 持久态 → 托管态 → 游离态;
  • MySQL 下谨记:事务要短、死锁要重试。