SQLAlchemy 的 expire_on_commit 参数

0 阅读4分钟

在 SQLAlchemy 的开发中,expire_on_commit 是一个非常关键的配置参数,尤其是在使用 异步 (Asyncio) 驱动或构建 Web API 时。

下面是一篇关于 expire_on_commit 的深度解析文章,帮助你彻底理解它的原理、作用以及为什么在你的代码中将其设置为 False


深度解析 SQLAlchemy 的 expire_on_commit 参数

在配置 SQLAlchemy 的 sessionmaker 时,我们经常会看到这样一行代码:

session_factory = async_sessionmaker(engine, expire_on_commit=False)

很多开发者习惯性地跟着教程将其设置为 False,但并不清楚为什么要这样做。本文将从底层原理出发,带你了解这个参数的“前世今生”。


一、 什么是 expire_on_commit

简单来说,expire_on_commit 决定了 在 Session 执行 commit() 提交事务后,当前 Session 里的数据库对象(Model 实例)是否还要保持“有效”状态。

  • 默认值:True
  • 作用对象: 处于 Session 追踪下的所有 Persistent(持久化)状态的对象。

二、 工作原理:过期与延迟加载

1. 当 expire_on_commit=True(默认值)时:

当你调用 await session.commit(),SQLAlchemy 会将当前 Session 中所有已经加载的对象属性全部 “过期化”(Expire)

  • 发生了什么: 对象在内存中依然存在,但它内部持有的数据(比如用户的姓名、邮箱等)被清空了。
  • 后果: 如果你在 commit 之后尝试访问 user.name,SQLAlchemy 会发现这个属性已过期,于是它会自动触发一次新的 SQL 查询(Lazy Load),去数据库里重新拉取最新的值。
2. 当 expire_on_commit=False 时:

执行 commit() 后,对象持有的数据依然保留在内存中,不会被标记为过期。

  • 后果: 在 commit 之后访问 user.name,直接从内存中读取,不会触发新的数据库查询。

三、 为什么异步编程(Async)通常设为 False

这是最核心的问题。在异步 SQLAlchemy 中,默认的 True 经常会导致程序崩溃。

1. 避免隐式的异步 IO 异常

在异步环境下,SQLAlchemy 要求所有的数据库操作必须通过 await 显式执行。 如果 expire_on_commit=True

  1. await session.commit()
  2. 你尝试打印 print(user.name)
  3. SQLAlchemy 尝试自动触发一次 SELECT 来刷新过期的数据。
  4. 报错! 因为 user.name 的访问是一个同步操作,而它触发的底层数据库查询是异步的。同步代码中无法处理异步 IO,系统会抛出 MissingGreenletAttributeError 相关的错误。
2. 性能考量

在 Web 开发中,我们通常的操作逻辑是: 查询数据 -> 修改数据 -> 提交 -> 将数据序列化并返回给前端。 如果设为 True,在返回给前端(序列化)的过程中,访问每个属性都会触发一次额外的 SELECT,这会导致严重的 N+1 查询问题,让原本高性能的异步程序变慢。


四、 代码对比示例

情况 A:expire_on_commit=True (可能报错或性能低)
async with session_factory() as session:
    user = await session.get(User, 1)
    user.last_login = datetime.now()
    await session.commit()  # 数据提交了,但 user 里的数据被清空了
    
    # 此时 user.name 是过期的
    print(user.name)  # 报错!试图在同步环境触发异步刷新
情况 B:expire_on_commit=False (安全、高效)
async with session_factory() as session:
    user = await session.get(User, 1)
    user.last_login = datetime.now()
    await session.commit()  # 提交了,user 里的数据依然留在内存中
    
    print(user.name)  # 正常打印,不产生额外的数据库查询

五、 这个参数会有负面影响吗?

既然 False 这么好,为什么官方默认是 True

原因在于:数据一致性。 如果 expire_on_commit=False,在你的事务提交后,如果 另外一个并发的程序 修改了数据库里的这条记录,你的 user 对象持有的依然是 commit 瞬间旧数据。

  • True 的逻辑: 每次提交后都重新查一遍,确保你看到的是数据库里最“新鲜”的数据。
  • False 的逻辑: 相信内存里的数据,不再去麻烦数据库。

在 Web 开发中,通常一个请求的生命周期非常短,我们并不担心在这个极短的时间内数据被其他进程修改,因此使用 False 是利大于弊的。


六、 总结与最佳实践

  1. 异步开发必设 False:在使用 async_sessionmaker 时,几乎永远应该设置 expire_on_commit=False
  2. Web 序列化友好:它能确保你在 commit 之后,依然可以轻松地将模型对象转化为 JSON 字典。
  3. 手动刷新:如果你确实需要获取数据库里最新的状态,可以使用 await session.refresh(instance) 手动触发刷新,而不是依赖自动过期。

一句话总结: expire_on_commit=False 是为了防止对象在事务提交后“失忆”,它是异步 SQLAlchemy 开发中的“免坑金牌”。