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、事务提交逻辑分开看。

依赖层负责生命周期。

业务层负责事务语义。

这样代码既稳定,也更容易维护。

posted @ 2026-09-13 21:43  fitch_liu  阅读(9)  评论(0)    收藏  举报