FastAPI 中 SQLAlchemy Session 的生命周期:从一次请求看懂资源边界
在 FastAPI 项目里,数据库 Session 最容易被误解成“全局可复用对象”。
实际上,它更应该被理解成一次业务操作中的数据库工作单元。
如果把 Session 放错位置,轻则连接泄漏,重则并发请求之间互相污染状态。
这篇文章用一次 HTTP 请求的执行过程,梳理 FastAPI 中 SQLAlchemy Session 的生命周期,以及项目里常见的几个坑。
1. Session 不是数据库连接本身
SQLAlchemy 的 Session 并不等同于一条数据库连接。
它更像是一个事务上下文和对象状态管理器。
它会在需要访问数据库时,从连接池里取出连接。
事务结束、Session 关闭后,连接再归还给连接池。
所以我们通常不会在应用启动时创建一个全局 Session,然后所有请求共用它。
正确的方式是:每次请求创建一个独立 Session,请求结束后关闭。
2. FastAPI 里常见的依赖写法
FastAPI 通常用依赖注入管理 Session 生命周期。
示例代码如下:
from collections.abc import Generator
from sqlalchemy.orm import Session
from app.db.session import SessionLocal
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()
接口中通过 Depends(get_db) 获取 Session:
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.models.user import User
router = APIRouter()
@router.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
return db.query(User).filter(User.id == user_id).first()
这段代码背后的生命周期是:
1. 请求进入接口前,FastAPI 调用 get_db()。 2. SessionLocal() 创建一个新的 Session。 3. yield db 把 Session 交给接口函数使用。 4. 接口返回或抛异常后,执行 finally。 5. db.close() 关闭 Session,并释放底层连接资源。
这里的关键不是“代码能不能查到数据”,而是“请求结束后资源有没有被稳定释放”。
3. 为什么不能用全局 Session
下面这种写法看起来省事,但在 Web 服务里很危险:
db = SessionLocal()
@router.get("/users/{user_id}")
def get_user(user_id: int):
return db.query(User).filter(User.id == user_id).first()
问题主要有三个。
第一,Session 内部维护对象状态缓存。
不同请求共用同一个 Session,可能读到不该复用的旧对象状态。
第二,Session 不是为跨线程、跨协程共享设计的。
并发请求同时操作同一个 Session,很容易出现状态竞争。
第三,异常请求可能让事务处于不干净状态。
如果没有及时 rollback 或 close,后续请求会踩到前一次请求留下的问题。
所以,全局可以放的是 engine 和 SessionLocal 工厂,不是具体的 Session 实例。
4. commit、rollback 和 close 的边界
close() 只负责关闭 Session,释放连接资源。
它不应该被当成业务成功的标志。
真正决定数据是否写入的是 commit()。
典型写入流程如下:
def create_user(db: Session, name: str):
user = User(name=name)
db.add(user)
try:
db.commit()
db.refresh(user)
return user
except Exception:
db.rollback()
raise
这里有两个边界要分清。
业务层决定什么时候 commit。
依赖层保证最后 close。
如果把 commit 全部藏进 get_db() 的 finally 里,接口代码看似干净,但事务边界会变得不清晰。
尤其是一个请求里有多个数据库操作时,什么时候提交、失败时回滚到哪里,会变得很难判断。
5. AsyncSession 也遵循同样原则
如果项目使用 SQLAlchemy 的异步模式,写法会变成 AsyncSession:
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import AsyncSessionLocal
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield session
查询也要使用 await:
from sqlalchemy import select
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
但核心原则没有变化。
每次请求拿到独立 Session。
请求结束释放 Session。
事务提交和回滚要有明确边界。
不要把 Session 当成全局共享对象。
6. 一个实用检查清单
排查 FastAPI 项目里的 Session 问题时,可以按下面几项检查。
- 是否只全局创建了
engine和SessionLocal,没有全局创建Session实例? - 每个接口是否通过依赖注入获取 Session?
yield依赖里是否保证了finally: db.close()?- 写操作失败时是否执行了
rollback()? - 是否把
commit()放在了清晰的业务边界上? - 异步项目是否全链路使用
AsyncSession,没有混用同步 Session?
7. 总结
FastAPI 中的 SQLAlchemy Session 管理,本质上是在管理资源边界和事务边界。
一句话概括:
> engine 可以全局复用,Session 应该随请求创建和释放。
把这个边界想清楚后,很多数据库连接泄漏、并发污染、事务状态异常的问题,都会更容易定位。
在实际项目里,建议把 get_db()、SessionLocal、事务提交逻辑分开看。
依赖层负责生命周期。
业务层负责事务语义。
这样代码既稳定,也更容易维护。

浙公网安备 33010602011771号