FastAPI 中间件、依赖注入与 SQLAlchemy 异步 ORM


目录

第一章 整体架构与核心概念

FastAPI 的请求处理可以抽象为一条完整链路:客户端发出 HTTP 请求,请求经过中间件后进入路由;FastAPI 解析路由参数并解析依赖关系;数据库依赖创建异步会话;路由通过 SQLAlchemy ORM 执行查询或数据变更;事务完成后释放会话并返回响应。

HTTP Request

查询与事务

Response

响应后置处理

客户端请求

HTTP 中间件

路由匹配与参数校验

依赖注入

公共参数

业务依赖

AsyncSession

SQLAlchemy 异步 ORM

MySQL

1.1 三个核心组件的职责

中间件

中间件位于 HTTP 请求与路由处理函数之间,同时包裹响应返回过程。它适合处理横跨多个接口的通用逻辑,例如:

  • 请求日志与响应日志
  • 接口耗时统计
  • 跨域处理
  • 请求头检查
  • 全局追踪标识
  • 统一的安全策略

中间件关注的是一次 HTTP 请求的完整生命周期,而不是某个具体接口的业务细节。

依赖注入

依赖注入负责声明并提供路由所需要的资源。路由只描述“需要什么”,FastAPI 根据 Depends() 自动执行依赖函数并把结果传入路由。

常见依赖包括:

  • 多个接口共享的分页参数
  • 当前登录用户
  • 权限校验结果
  • 数据库会话
  • 公共配置或服务对象

依赖注入能够把资源创建、参数校验、异常处理和资源释放从路由函数中分离出来。

SQLAlchemy 异步 ORM

ORM(Object-Relational Mapping,对象关系映射)在 Python 对象和关系型数据库表之间建立映射:

Python 类 Book       <──映射──>    数据库表 book
Book.id              <──映射──>    字段 id
Book.bookname        <──映射──>    字段 bookname
Book 实例            <──映射──>    表中的一行记录

SQLAlchemy 异步 API 使用 async/await 等待数据库 I/O。在等待数据库返回结果期间,事件循环可以处理其他可运行任务,从而提高 I/O 密集型服务的并发利用率。异步并不意味着单条 SQL 会执行得更快,其价值主要体现在等待期间不阻塞当前事件循环。

1.2 关键术语

术语 含义
FastAPI 基于 Python 类型提示构建 API 的 Web 框架
路由 HTTP 方法、URL 路径与处理函数之间的映射
中间件 包裹请求与响应过程的通用处理层
依赖项 由 FastAPI 解析、执行并注入路由的函数或可调用对象
Depends 声明路由依赖关系的工具
ORM 将数据库表、字段和记录映射为类、属性和对象
异步引擎 管理异步数据库连接与连接池的 AsyncEngine
会话 通过 AsyncSession 执行语句并管理 ORM 对象和事务
事务 作为一个整体提交或回滚的一组数据库操作
DeclarativeBase SQLAlchemy 2.0 声明式 ORM 模型基类
Mapped ORM 属性的类型标注容器
mapped_column 声明模型字段及其数据库约束
CRUD Create、Read、Update、Delete,即新增、查询、更新、删除
标量值 单个值,例如总数、平均价格或单条 ORM 对象
结果集 数据库语句执行后返回的 Result 对象

1.3 示例代码的公共结构

异步 ORM 的基础设施通常由以下组成部分构成:

  1. 创建 FastAPI 应用;
  2. 创建 SQLAlchemy 异步引擎;
  3. 声明 BaseBook ORM 模型;
  4. 在应用启动时建表;
  5. 创建异步会话工厂;
  6. 通过依赖函数提供数据库会话;
  7. 在路由中执行不同的 CRUD 操作。

这些部分构成一套可复用的 ORM 基础设施;具体业务接口只需在此基础上编写不同的查询或数据变更逻辑。第二章先展示公共模板,再讨论各类操作的差异。


第二章 中间件、依赖注入与异步 ORM

2.1 HTTP 中间件:包裹完整请求周期

FastAPI 使用 @app.middleware("http") 注册 HTTP 中间件。中间件函数接收两个核心参数:

  • request:当前请求对象;
  • call_next:将请求继续传递给下一处理层,并返回响应。
@app.middleware("http")
async def middleware2(request, call_next):
    print("中间件2 start")
    response = await call_next(request)
    print("中间件2 end")
    return response

@app.middleware("http")
async def middleware1(request, call_next):
    print("中间件1 start")
    response = await call_next(request)
    print("中间件1 end")
    return response

await call_next(request) 是执行顺序的分界点:

  • 调用之前属于请求前置处理;
  • 调用之后属于响应后置处理;
  • 返回的 response 必须继续向外层返回。

在该示例的注册顺序下,中间件形成嵌套结构:

路由函数 Middleware 2 Middleware 1 客户端 路由函数 Middleware 2 Middleware 1 客户端 middleware1 start middleware2 start middleware2 end middleware1 end HTTP 请求 call_next(request) 调用路由 返回响应 返回响应 HTTP 响应

其结构与嵌套函数调用类似,后注册的中间件位于更外层。需要注意,中间件应保留全局、轻量的职责;只服务于个别路由的逻辑通常更适合放在依赖项或路由函数中。


2.2 依赖注入:公共参数与资源管理

新闻列表与用户列表都需要 skiplimit。若每个路由分别声明,校验规则和默认值容易产生差异。示例将公共参数封装为依赖:

async def common_parameters(
    skip: int = Query(0, ge=0),
    limit: int = Query(10, le=60)
):
    return {"skip": skip, "limit": limit}

@app.get("/news/news_list")
async def get_news_list(commons=Depends(common_parameters)):
    return commons

@app.get("/user/user_list")
async def get_user_list(commons=Depends(common_parameters)):
    return commons

调用链可以表示为:

接收请求参数

解析公共依赖

校验分页参数

生成依赖返回值

注入路由参数

skip 等于 20

limit 等于 10

skip 大于等于 0

limit 小于等于 60

Depends(common_parameters) 声明的是依赖关系,而不是在定义路由时立即调用函数。FastAPI 会在请求期间解析该关系,并把依赖函数的返回值注入 commons

依赖函数还可以使用 yieldyield 前负责创建资源,yield 后负责清理资源,因此非常适合数据库会话等具有生命周期的对象:

yield session

正常结束

发生异常

创建AsyncSession

路由使用会话

提交事务

回滚事务

关闭会话


2.3 SQLAlchemy 异步 ORM 公共模板

以下模板整合了异步 ORM 中反复使用的数据库基础代码。为避免在每个 CRUD 操作中重复展示,后续只保留实际变化的语句。

from datetime import datetime

from fastapi import Depends, FastAPI
from sqlalchemy import DateTime, Float, String, func
from sqlalchemy.ext.asyncio import (
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

app = FastAPI()

ASYNC_DATABASE_URL = (
    "mysql+aiomysql://root:123456@localhost:3306/"
    "FastAPI_first?charset=utf8"
)

async_engine = create_async_engine(
    ASYNC_DATABASE_URL,
    echo=True,
    pool_size=10,
    max_overflow=20,
)


class Base(DeclarativeBase):
    create_time: Mapped[datetime] = mapped_column(
        DateTime,
        insert_default=func.now(),
        default=func.now,
        comment="创建时间",
    )
    update_time: Mapped[datetime] = mapped_column(
        DateTime,
        insert_default=func.now(),
        default=func.now,
        onupdate=func.now(),
        comment="修改时间",
    )


class Book(Base):
    __tablename__ = "book"

    id: Mapped[int] = mapped_column(primary_key=True, comment="书籍id")
    bookname: Mapped[str] = mapped_column(String(255), comment="书名")
    author: Mapped[str] = mapped_column(String(255), comment="作者")
    price: Mapped[float] = mapped_column(Float, comment="价格")
    publisher: Mapped[str] = mapped_column(String(255), comment="出版社")


async def create_tables():
    async with async_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


@app.on_event("startup")
async def startup_event():
    await create_tables()


AsyncSessionLocal = async_sessionmaker(
    bind=async_engine,
    class_=AsyncSession,
    expire_on_commit=False,
)


async def get_database():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise

异步引擎与连接池

连接字符串:

mysql+aiomysql://root:123456@localhost:3306/FastAPI_first?charset=utf8
│     │            │    │         │
│     │            │    │         └─ 数据库名称与字符集
│     │            │    └────────── 主机与端口
│     │            └─────────────── 用户名与密码
│     └──────────────────────────── 异步驱动
└────────────────────────────────── 数据库方言

主要配置项如下:

参数 作用
echo=True 输出 SQL 日志,适合开发调试
pool_size=10 连接池常驻连接数量
max_overflow=20 连接池耗尽时允许临时增加的连接数量

连接池并不是为每次请求永久创建新连接,而是维护并复用一组连接。实际配置应结合数据库最大连接数、应用实例数量与请求并发量确定。

声明式模型

Book 类通过 __tablename__ = "book" 绑定数据库表。Mapped[T] 表示 Python 属性类型,mapped_column() 描述数据库字段约束。

Book.id: Mapped[int]
   │             │
   │             └─ Python 类型标注
   └─────────────── ORM 属性

mapped_column(primary_key=True)
                 │
                 └─ 数据库主键约束

Base 中的 create_timeupdate_time 会被继承到 Book 模型中,因此公共审计字段只需声明一次。

建表过程

async with async_engine.begin() as conn:
    await conn.run_sync(Base.metadata.create_all)

Base.metadata 收集所有继承自 Base 的表元数据;create_all() 根据元数据创建尚不存在的表。run_sync() 用于在异步连接中执行 SQLAlchemy 提供的同步 DDL 操作。

示例使用 @app.on_event("startup") 在启动时建表。该方式便于演示;在正式项目中,表结构变更通常交给 Alembic 等迁移工具管理,以保存版本历史并支持升级、回退。

会话工厂与事务边界

async_sessionmaker() 创建异步会话工厂。每次调用 AsyncSessionLocal() 都会生成一个独立的 AsyncSession

async def get_database():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise

该依赖定义了一条清晰的事务规则:

路由正常结束  ──> commit
路由发生异常  ──> rollback ──> 继续抛出异常
请求处理结束  ──> async with 自动关闭会话

expire_on_commit=False 表示提交后不立即使 ORM 对象中的属性过期,因此返回对象时通常不需要为已加载字段再次查询数据库。


2.4 路由中使用异步会话

@app.get("/book/books")
async def get_book_list(db: AsyncSession = Depends(get_database)):
    result = await db.execute(select(Book))
    books = result.scalars().all()
    return books

执行过程如下:

  1. FastAPI 发现 Depends(get_database)
  2. get_database() 创建并产出 AsyncSession
  3. 路由使用 db.execute() 异步执行查询;
  4. scalars() 从结果行中提取 Book 对象;
  5. all() 将全部对象收集为列表;
  6. 路由结束后,依赖函数继续执行事务处理并关闭会话。

这里需要区分三个对象:

对象 作用
select(Book) SQL 表达式,描述需要执行的查询
result 数据库执行结果,可能包含多行、多列
result.scalars().all() 提取第一列中的 ORM 对象并形成列表

2.5 查询:从主键定位到聚合与分页

主键查询与普通查询

按主键获取单条记录时,AsyncSession.get() 最直接:

book = await db.get(Book, 1)

条件查询则通过 select()where() 构造:

result = await db.execute(
    select(Book).where(Book.id == book_id)
)
book = result.scalar_one_or_none()

两种方式的侧重点不同:

  • db.get(Book, primary_key):按照主键获取单个 ORM 对象;
  • select(Book).where(...):支持任意查询条件、排序、关联、分页与组合表达式。

结果提取方法也对应不同语义:

方法 返回内容 典型用途
scalar_one_or_none() 一条记录或 None;多于一条会报错 唯一条件查询
scalars().first() 第一条记录或 None 只关心第一条
scalars().all() ORM 对象列表 列表查询
scalar() 结果第一行第一列 聚合值或单个值

比较、模糊、逻辑与包含条件

# 等值条件
select(Book).where(Book.id == book_id)

# 范围条件
select(Book).where(Book.price >= 200)

# 模糊条件:作者以“曹”开头
select(Book).where(Book.author.like("曹%"))

# OR 条件
select(Book).where(
    (Book.author.like("曹%")) | (Book.price > 100)
)

# IN 条件
id_list = [1, 3, 5, 7]
select(Book).where(Book.id.in_(id_list))

LIKE 通配符的含义如下:

通配符 含义 示例
% 任意数量字符 曹% 匹配所有以“曹”开头的内容
_ 单个字符 曹_ 匹配“曹”后恰好一个字符

SQLAlchemy 表达式中的逻辑运算符为:

  • &:AND;
  • |:OR;
  • ~:NOT。

组合条件时应使用括号明确优先级,避免 Python 运算符优先级导致表达式含义偏离预期。

聚合查询

count_stmt = select(func.count(Book.id))
max_stmt = select(func.max(Book.price))
sum_stmt = select(func.sum(Book.price))
avg_stmt = select(func.avg(Book.price))

result = await db.execute(avg_stmt)
average_price = result.scalar()

聚合函数返回单个计算值,因此通常使用 scalar() 提取。聚合查询不会返回完整 Book 对象,而是返回数量、最大值、总和或平均值等结果。

分页查询

skip = (page - 1) * page_size
stmt = select(Book).offset(skip).limit(page_size)
result = await db.execute(stmt)
books = result.scalars().all()

分页偏移量公式为:

offset = (page - 1) × page_size

例如 page=3page_size=10 时,需要跳过前 20 条记录,再获取 10 条。

分页查询通常还应补充稳定排序:

stmt = (
    select(Book)
    .order_by(Book.id)
    .offset(skip)
    .limit(page_size)
)

没有明确的 order_by() 时,数据库不保证每次返回相同顺序,可能导致翻页时出现重复或遗漏。


2.6 新增、更新与删除

新增数据

请求体由 Pydantic 模型负责校验,随后转换为 ORM 对象并加入会话:

class BookCreate(BaseModel):
    id: int
    bookname: str
    author: str
    price: float
    publisher: str


@app.post("/book/add_book")
async def add_book(
    book: BookCreate,
    db: AsyncSession = Depends(get_database),
):
    book_obj = Book(**book.model_dump())
    db.add(book_obj)
    return book_obj

数据流向如下:

JSON 请求体
    ↓ Pydantic 校验
BookCreate 对象
    ↓ model_dump()
字典
    ↓ Book(**data)
ORM 对象
    ↓ db.add()
加入当前会话
    ↓ 依赖结束后 commit
写入数据库

原示例使用 book.__dict__ 并在路由中显式 commit()。在 Pydantic 2 中,model_dump() 是更明确的模型导出方式;如果事务已经由 get_database() 统一提交,路由通常不需要重复提交。

更新数据

更新采用“先查询,再修改”的对象化方式:

@app.put("/book/update_book/{book_id}")
async def update_book(
    book_id: int,
    data: BookUpdate,
    db: AsyncSession = Depends(get_database),
):
    db_book = await db.get(Book, book_id)
    if db_book is None:
        raise HTTPException(status_code=404, detail="查无此书")

    db_book.bookname = data.bookname
    db_book.author = data.author
    db_book.price = data.price
    db_book.publisher = data.publisher
    return db_book

被会话管理的 ORM 对象发生属性变化后,SQLAlchemy 会跟踪这些变化。事务提交时,对应的 UPDATE 语句会被发送到数据库。

删除数据

删除同样先确认目标记录存在:

@app.delete("/book/delete_book/{book_id}")
async def delete_book(
    book_id: int,
    db: AsyncSession = Depends(get_database),
):
    db_book = await db.get(Book, book_id)
    if db_book is None:
        raise HTTPException(status_code=404, detail="查无此书")

    await db.delete(db_book)
    return {"msg": "删除图书成功"}

路由正常结束后,由会话依赖统一提交删除事务。若查询不到记录,抛出 HTTPException(404);异常会使依赖执行回滚,避免提交不完整的事务。

CRUD 规律汇总

操作 主要步骤 核心 API
新增 构造对象 → 加入会话 → 提交 Book(...)db.add()
查询 构造语句 → 执行 → 提取结果 select()db.execute()scalars()
主键查询 指定模型和主键 db.get()
更新 查询对象 → 修改属性 → 提交 db.get()、属性赋值
删除 查询对象 → 标记删除 → 提交 db.get()db.delete()

第三章 总结

中间件、依赖注入与 SQLAlchemy 异步 ORM 分别对应 FastAPI 服务中的三个关键环节:请求处理、资源组织与数据持久化。

  • 中间件包裹一次完整的 HTTP 请求与响应过程。call_next(request) 之前可执行请求前置逻辑,之后可执行响应后置逻辑,适合承载日志、耗时统计等通用能力。
  • 依赖注入通过 Depends() 将公共参数、鉴权结果和数据库会话等资源按需提供给路由。依赖函数中的 yield 能够自然表达“创建资源 → 使用资源 → 清理资源”的生命周期。
  • SQLAlchemy 异步 ORM使用模型描述表结构,使用 AsyncSession 执行异步查询与 CRUD,并通过 commit()rollback() 维护事务一致性。

一次典型请求的处理过程可以概括为:

请求进入
  → 中间件执行前置处理
  → FastAPI 解析路由与依赖
  → 路由使用 AsyncSession 执行数据库操作
  → 正常提交或异常回滚事务
  → 中间件执行后置处理
  → 返回响应

数据库操作可以归纳为四类固定模式:

操作 核心过程 常用 API
查询 构造语句 → 执行 → 提取结果 select()execute()scalars()
新增 构造 ORM 对象 → 加入会话 → 提交 Book(...)add()commit()
更新 查询目标 → 修改属性 → 提交 get()、属性赋值、commit()
删除 查询目标 → 删除对象 → 提交 get()delete()commit()

因此,路由函数只需聚焦于接口行为本身:接收参数、调用依赖、执行相应的数据操作并返回结果。中间件负责通用请求过程,依赖项负责可复用资源,ORM 负责数据访问;明确这些职责边界后,接口代码会更简洁,也更容易维护。

后面我会继续更新fastapi项目部分和langggraph进阶部分,期待关注,我们共同进步!

更多推荐