SQLAlchemy会话意外失效引发PendingRollbackError及FastAPI会话使用咨询
我在FastAPI项目中使用SQLAlchemy 2.0.20作为ORM库。每次向API服务器发起请求时,路由需要一个sqlalchemy.orm.session.Session对象,我要么通过DbSession = sessionmaker(engine)重新创建,要么从缓存中获取,代码如下:
engine = create_engine( connection_string(), poolclass=QueuePool, pool_size=15, pool_recycle=3600, ) DbSession = sessionmaker(engine) @cache def get_db_session() -> Session: session = DbSession() return session # 该会话被用于FastAPI路由
后来API突然无法正常运行,原因是缓存的会话对象被标记为无效(具体原因未知),使用会话时抛出错误:PendingRollbackError: Can't reconnect until invalid transaction is rolled back。
我的仓库类同样被跨请求缓存,代码如下:
class SqlRepository: # 该仓库对象也会跨API请求被缓存(和会话一样) def __init__(session: Session): self.session = session def get_all_todos(self): self.session.query(TodoORM).all() # 此处抛出PendingRollbackError ...
我的问题:
- 是否应为每个API请求重新创建会话(即不缓存会话)?
- 是否应在
get_all_todos中处理PendingRollbackError?
另外,我缓存会话的初衷是想复用连接池中的连接,使用一个只需实例化一次的长生命周期会话。请问传入会话实例化仓库的模式是否正确?还是仓库应持有sessionmaker,每次执行数据库事务时重新创建会话?
1. 是否应为每个API请求重新创建会话?
必须为每个请求创建新会话,绝对不能缓存会话。
SQLAlchemy的Session设计就是短生命周期、请求级别的:
- Session会维护自身的事务状态(比如未提交的修改、事务上下文),跨请求复用会导致事务状态混乱,比如前一个请求的未回滚事务污染后续请求。
- 当Session对应的连接被池回收、或者数据库端断开连接时,Session会进入无效状态,此时复用就会抛出你遇到的
PendingRollbackError。 - 连接池本身已经帮你处理了连接复用,无需通过缓存Session来实现连接复用——Session每次需要数据库操作时,会自动从连接池获取可用连接,操作完成后(Session关闭时)将连接归还池。
FastAPI的最佳实践就是用依赖项为每个请求生成新Session:
def get_db_session() -> Generator[Session, None, None]: session = DbSession() try: yield session session.commit() except Exception: session.rollback() raise finally: session.close() # 路由中使用 @app.get("/todos") def get_todos(db: Session = Depends(get_db_session)): repo = SqlRepository(db) return repo.get_all_todos()
2. 是否应在get_all_todos中处理PendingRollbackError?
不需要,也不应该。
这个错误本质是Session生命周期管理不当导致的,修复Session的创建/销毁逻辑就能从根源解决问题,而不是在业务代码里做异常兜底。如果坚持缓存Session,即使处理了这个错误,后续还会出现其他事务状态相关的问题(比如脏读、重复提交),无法彻底解决。
关于仓库模式的选择
传入请求级别的Session实例化仓库是正确的模式,但要注意:
- 仓库对象也不能跨请求缓存,应该为每个请求创建新的仓库实例,传入当前请求的Session。
- 不要让仓库持有
sessionmaker并自行创建Session——这样会把事务管理的逻辑分散到仓库中,不利于统一控制(比如全局的事务提交/回滚、Session关闭)。
正确的做法是:
- 用FastAPI依赖项提供请求级别的Session。
- 路由中获取Session,实例化仓库。
- 仓库仅负责封装数据库操作,依赖传入的Session完成事务。
这样既保证了Session的正确生命周期,又能通过连接池复用数据库连接,同时保持代码的清晰分层。
内容的提问来源于stack exchange,提问作者tenticon

