FastAPI + SQLAlchemy Async,从建表到 CRUD 一条线讲透

学 FastAPI 到数据库这里,很多人会突然觉得难度上来了。

前面只是写函数、收参数、返回 JSON。

到了 ORM,就开始出现一堆新东西:

python 复制代码
create_async_engine
async_sessionmaker
AsyncSession
DeclarativeBase
mapped_column
select
commit
rollback

名字都认识,但放在一起就乱。

其实这条线并不复杂。

你只要把它理解成一次请求里的数据库工作流就行:

客户端发请求,FastAPI 创建数据库会话,路由函数用 ORM 查询或修改数据,最后提交或回滚事务。

这一篇就把这条线从头到尾串起来。

本篇准备

这一篇开始需要数据库相关依赖:

bash 复制代码
pip install fastapi uvicorn sqlalchemy aiomysql

如果你准备用 .env 管理数据库连接字符串,再安装:

bash 复制代码
pip install python-dotenv

本文示例默认你已经有一个 MySQL 数据库,可以先建一个练习库:

sql 复制代码
CREATE DATABASE news_app DEFAULT CHARACTER SET utf8mb4;

生产环境不要把账号密码硬编码在代码里。课程演示可以直接写连接字符串,但真实项目更建议放到环境变量或 .env 文件。

1. ORM 到底解决什么问题

不用 ORM 时,你直接写 SQL:

sql 复制代码
SELECT * FROM book WHERE id = 1;

用了 SQLAlchemy ORM,你写 Python:

python 复制代码
book = await db.get(Book, 1)

ORM 的核心作用是,把数据库表映射成 Python 类,把表中的一行映射成 Python 对象。
#mermaid-svg-yhTuzeM5Cb6XZHzk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yhTuzeM5Cb6XZHzk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yhTuzeM5Cb6XZHzk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yhTuzeM5Cb6XZHzk .error-icon{fill:#552222;}#mermaid-svg-yhTuzeM5Cb6XZHzk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yhTuzeM5Cb6XZHzk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yhTuzeM5Cb6XZHzk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yhTuzeM5Cb6XZHzk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yhTuzeM5Cb6XZHzk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yhTuzeM5Cb6XZHzk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yhTuzeM5Cb6XZHzk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yhTuzeM5Cb6XZHzk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yhTuzeM5Cb6XZHzk .marker.cross{stroke:#333333;}#mermaid-svg-yhTuzeM5Cb6XZHzk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yhTuzeM5Cb6XZHzk p{margin:0;}#mermaid-svg-yhTuzeM5Cb6XZHzk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yhTuzeM5Cb6XZHzk .cluster-label text{fill:#333;}#mermaid-svg-yhTuzeM5Cb6XZHzk .cluster-label span{color:#333;}#mermaid-svg-yhTuzeM5Cb6XZHzk .cluster-label span p{background-color:transparent;}#mermaid-svg-yhTuzeM5Cb6XZHzk .label text,#mermaid-svg-yhTuzeM5Cb6XZHzk span{fill:#333;color:#333;}#mermaid-svg-yhTuzeM5Cb6XZHzk .node rect,#mermaid-svg-yhTuzeM5Cb6XZHzk .node circle,#mermaid-svg-yhTuzeM5Cb6XZHzk .node ellipse,#mermaid-svg-yhTuzeM5Cb6XZHzk .node polygon,#mermaid-svg-yhTuzeM5Cb6XZHzk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yhTuzeM5Cb6XZHzk .rough-node .label text,#mermaid-svg-yhTuzeM5Cb6XZHzk .node .label text,#mermaid-svg-yhTuzeM5Cb6XZHzk .image-shape .label,#mermaid-svg-yhTuzeM5Cb6XZHzk .icon-shape .label{text-anchor:middle;}#mermaid-svg-yhTuzeM5Cb6XZHzk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yhTuzeM5Cb6XZHzk .rough-node .label,#mermaid-svg-yhTuzeM5Cb6XZHzk .node .label,#mermaid-svg-yhTuzeM5Cb6XZHzk .image-shape .label,#mermaid-svg-yhTuzeM5Cb6XZHzk .icon-shape .label{text-align:center;}#mermaid-svg-yhTuzeM5Cb6XZHzk .node.clickable{cursor:pointer;}#mermaid-svg-yhTuzeM5Cb6XZHzk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yhTuzeM5Cb6XZHzk .arrowheadPath{fill:#333333;}#mermaid-svg-yhTuzeM5Cb6XZHzk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yhTuzeM5Cb6XZHzk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yhTuzeM5Cb6XZHzk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yhTuzeM5Cb6XZHzk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yhTuzeM5Cb6XZHzk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yhTuzeM5Cb6XZHzk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yhTuzeM5Cb6XZHzk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yhTuzeM5Cb6XZHzk .cluster text{fill:#333;}#mermaid-svg-yhTuzeM5Cb6XZHzk .cluster span{color:#333;}#mermaid-svg-yhTuzeM5Cb6XZHzk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yhTuzeM5Cb6XZHzk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yhTuzeM5Cb6XZHzk rect.text{fill:none;stroke-width:0;}#mermaid-svg-yhTuzeM5Cb6XZHzk .icon-shape,#mermaid-svg-yhTuzeM5Cb6XZHzk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yhTuzeM5Cb6XZHzk .icon-shape p,#mermaid-svg-yhTuzeM5Cb6XZHzk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yhTuzeM5Cb6XZHzk .icon-shape .label rect,#mermaid-svg-yhTuzeM5Cb6XZHzk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yhTuzeM5Cb6XZHzk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yhTuzeM5Cb6XZHzk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yhTuzeM5Cb6XZHzk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Python 类 Book
数据库表 book
Book 对象
book 表中的一行
对象属性 book.name
表字段 name

这样你就可以用更接近 Python 的方式操作数据库。

2. 创建异步引擎

SQLAlchemy Async 的第一步,是创建异步引擎。

python 复制代码
from sqlalchemy.ext.asyncio import create_async_engine

DATABASE_URL = "mysql+aiomysql://root:password@127.0.0.1:3306/news_app"

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

这里有几个点:

配置 含义
mysql+aiomysql 使用 MySQL,并通过 aiomysql 做异步驱动
echo=True 打印 SQL,适合开发调试
pool_size 连接池基础连接数
max_overflow 连接池不够时允许额外创建的连接数

生产环境不要把账号密码硬编码在代码里。

更好的方式是放到 .env 或环境变量里。

课程为了教学写死连接字符串能理解,但真实项目要改。

3. 定义 ORM 模型

模型类表示数据库表。

课程里图书模型大概是这样:

python 复制代码
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Float

class Base(DeclarativeBase):
    pass

class Book(Base):
    __tablename__ = "book"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    bookname: Mapped[str] = mapped_column(String(50))
    author: Mapped[str] = mapped_column(String(50))
    price: Mapped[float] = mapped_column(Float)
    publisher: Mapped[str] = mapped_column(String(100))

这段代码表达的是:

  • Book 类对应 book
  • id 是主键
  • booknameauthorpricepublisher 是字段

SQLAlchemy 2.0 推荐使用 Mappedmapped_column 这种类型友好的写法。

它比旧式 Column(...) 更适合现代 Python 类型提示。

4. 建表,学习阶段可以自动建,真实项目要迁移工具

课程里会用类似这样的代码启动时建表:

python 复制代码
async def create_tables():
    async with async_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

再挂到应用启动阶段:

python 复制代码
@app.on_event("startup")
async def startup():
    await create_tables()

或者用新项目更推荐的 lifespan:

python 复制代码
from contextlib import asynccontextmanager
from fastapi import FastAPI

@asynccontextmanager
async def lifespan(app: FastAPI):
    await create_tables()
    yield

app = FastAPI(lifespan=lifespan)

学习阶段自动建表很方便。

但真实生产项目通常会用 Alembic 这类迁移工具管理表结构变化。

因为线上数据库不是每次启动都随便 create_all 的。

5. 创建会话工厂

引擎负责连接数据库,但每次请求真正操作数据库,通常靠 AsyncSession

先创建会话工厂:

python 复制代码
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession

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

async_sessionmaker 不是一个具体会话。

它是生产会话的工厂。

每次调用它,得到一个新的 AsyncSession

SQLAlchemy 官方文档里也说明,调用 async_sessionmaker 实例会基于配置生成新的 AsyncSession

这就是为什么我们通常把它放进依赖函数里。

6. 用 Depends 注入数据库会话

python 复制代码
async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
        except Exception:
            await session.rollback()
            raise

路由里使用:

python 复制代码
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

@app.get("/book/list")
async def get_book_list(db: AsyncSession = Depends(get_db)):
    ...

一次请求里的数据库链路就变成:
MySQL AsyncSession get_db Route Client MySQL AsyncSession get_db Route Client #mermaid-svg-2pviyQDSEfKYKy9q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2pviyQDSEfKYKy9q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2pviyQDSEfKYKy9q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2pviyQDSEfKYKy9q .error-icon{fill:#552222;}#mermaid-svg-2pviyQDSEfKYKy9q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2pviyQDSEfKYKy9q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2pviyQDSEfKYKy9q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2pviyQDSEfKYKy9q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2pviyQDSEfKYKy9q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2pviyQDSEfKYKy9q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2pviyQDSEfKYKy9q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2pviyQDSEfKYKy9q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2pviyQDSEfKYKy9q .marker.cross{stroke:#333333;}#mermaid-svg-2pviyQDSEfKYKy9q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2pviyQDSEfKYKy9q p{margin:0;}#mermaid-svg-2pviyQDSEfKYKy9q .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2pviyQDSEfKYKy9q text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-2pviyQDSEfKYKy9q .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2pviyQDSEfKYKy9q .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-2pviyQDSEfKYKy9q .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-2pviyQDSEfKYKy9q .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-2pviyQDSEfKYKy9q #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-2pviyQDSEfKYKy9q .sequenceNumber{fill:white;}#mermaid-svg-2pviyQDSEfKYKy9q #sequencenumber{fill:#333;}#mermaid-svg-2pviyQDSEfKYKy9q #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-2pviyQDSEfKYKy9q .messageText{fill:#333;stroke:none;}#mermaid-svg-2pviyQDSEfKYKy9q .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2pviyQDSEfKYKy9q .labelText,#mermaid-svg-2pviyQDSEfKYKy9q .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-2pviyQDSEfKYKy9q .loopText,#mermaid-svg-2pviyQDSEfKYKy9q .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-2pviyQDSEfKYKy9q .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2pviyQDSEfKYKy9q .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2pviyQDSEfKYKy9q .noteText,#mermaid-svg-2pviyQDSEfKYKy9q .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-2pviyQDSEfKYKy9q .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2pviyQDSEfKYKy9q .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2pviyQDSEfKYKy9q .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2pviyQDSEfKYKy9q .actorPopupMenu{position:absolute;}#mermaid-svg-2pviyQDSEfKYKy9q .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-2pviyQDSEfKYKy9q .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2pviyQDSEfKYKy9q .actor-man circle,#mermaid-svg-2pviyQDSEfKYKy9q line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-2pviyQDSEfKYKy9q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发起请求 Depends(get_db) 创建 session 注入 session 执行 ORM 操作 SQL 返回结果 ORM 对象 返回响应

这就是 FastAPI + SQLAlchemy Async 最核心的结构。

这里我建议先采用一种更容易读懂的约定:

get_db() 只负责提供会话,并在异常时回滚。

写操作在哪里发生,commit() 就在哪里显式调用。

这样读代码的人一眼能看出来,哪个接口真的修改了数据库。

7. 查询数据

查询全部:

python 复制代码
from sqlalchemy import select

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

按主键查:

python 复制代码
book = await db.get(Book, book_id)

条件查询:

python 复制代码
result = await db.execute(
    select(Book).where(Book.price >= 100)
)
books = result.scalars().all()

模糊查询:

python 复制代码
result = await db.execute(
    select(Book).where(Book.author.like("曹%"))
)

包含查询:

python 复制代码
result = await db.execute(
    select(Book).where(Book.id.in_([1, 3, 5]))
)

聚合查询:

python 复制代码
from sqlalchemy import func

result = await db.execute(select(func.count(Book.id)))
count = result.scalar()

这里最容易混的是结果提取方式。

写法 适合场景
result.scalars().all() 多条 ORM 对象
result.scalars().first() 第一条 ORM 对象
result.scalar_one_or_none() 期望最多一条结果
result.scalar() 聚合值,比如 count、avg
await db.get(Model, id) 按主键查一条

记住一个小规则:

select(Book),通常拿的是模型对象。

select(func.count(Book.id)),拿的是数字。

8. 分页查询

分页公式是:

text 复制代码
offset = (page - 1) * page_size

示例:

python 复制代码
from fastapi import Query
from sqlalchemy import select

@app.get("/book/list")
async def get_book_list(
    page: int = Query(1, ge=1),
    page_size: int = Query(10, ge=1, le=100),
    db: AsyncSession = Depends(get_db)
):
    offset = (page - 1) * page_size
    stmt = select(Book).offset(offset).limit(page_size)
    result = await db.execute(stmt)
    return result.scalars().all()

流程是:
#mermaid-svg-IjQegPsarqZXVejl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IjQegPsarqZXVejl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IjQegPsarqZXVejl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IjQegPsarqZXVejl .error-icon{fill:#552222;}#mermaid-svg-IjQegPsarqZXVejl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IjQegPsarqZXVejl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IjQegPsarqZXVejl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IjQegPsarqZXVejl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IjQegPsarqZXVejl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IjQegPsarqZXVejl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IjQegPsarqZXVejl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IjQegPsarqZXVejl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IjQegPsarqZXVejl .marker.cross{stroke:#333333;}#mermaid-svg-IjQegPsarqZXVejl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IjQegPsarqZXVejl p{margin:0;}#mermaid-svg-IjQegPsarqZXVejl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IjQegPsarqZXVejl .cluster-label text{fill:#333;}#mermaid-svg-IjQegPsarqZXVejl .cluster-label span{color:#333;}#mermaid-svg-IjQegPsarqZXVejl .cluster-label span p{background-color:transparent;}#mermaid-svg-IjQegPsarqZXVejl .label text,#mermaid-svg-IjQegPsarqZXVejl span{fill:#333;color:#333;}#mermaid-svg-IjQegPsarqZXVejl .node rect,#mermaid-svg-IjQegPsarqZXVejl .node circle,#mermaid-svg-IjQegPsarqZXVejl .node ellipse,#mermaid-svg-IjQegPsarqZXVejl .node polygon,#mermaid-svg-IjQegPsarqZXVejl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IjQegPsarqZXVejl .rough-node .label text,#mermaid-svg-IjQegPsarqZXVejl .node .label text,#mermaid-svg-IjQegPsarqZXVejl .image-shape .label,#mermaid-svg-IjQegPsarqZXVejl .icon-shape .label{text-anchor:middle;}#mermaid-svg-IjQegPsarqZXVejl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IjQegPsarqZXVejl .rough-node .label,#mermaid-svg-IjQegPsarqZXVejl .node .label,#mermaid-svg-IjQegPsarqZXVejl .image-shape .label,#mermaid-svg-IjQegPsarqZXVejl .icon-shape .label{text-align:center;}#mermaid-svg-IjQegPsarqZXVejl .node.clickable{cursor:pointer;}#mermaid-svg-IjQegPsarqZXVejl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IjQegPsarqZXVejl .arrowheadPath{fill:#333333;}#mermaid-svg-IjQegPsarqZXVejl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IjQegPsarqZXVejl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IjQegPsarqZXVejl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IjQegPsarqZXVejl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IjQegPsarqZXVejl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IjQegPsarqZXVejl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IjQegPsarqZXVejl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IjQegPsarqZXVejl .cluster text{fill:#333;}#mermaid-svg-IjQegPsarqZXVejl .cluster span{color:#333;}#mermaid-svg-IjQegPsarqZXVejl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IjQegPsarqZXVejl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IjQegPsarqZXVejl rect.text{fill:none;stroke-width:0;}#mermaid-svg-IjQegPsarqZXVejl .icon-shape,#mermaid-svg-IjQegPsarqZXVejl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IjQegPsarqZXVejl .icon-shape p,#mermaid-svg-IjQegPsarqZXVejl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IjQegPsarqZXVejl .icon-shape .label rect,#mermaid-svg-IjQegPsarqZXVejl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IjQegPsarqZXVejl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IjQegPsarqZXVejl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IjQegPsarqZXVejl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} page/page_size
计算 offset
select(Book)
offset(offset)
limit(page_size)
db.execute
scalars().all()

在新闻项目里,新闻列表、收藏列表、浏览历史列表都用到了分页。

9. 新增、更新、删除

新增数据:

python 复制代码
from pydantic import BaseModel

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

@app.post("/book")
async def create_book(data: BookCreate, db: AsyncSession = Depends(get_db)):
    book = Book(**data.model_dump())
    db.add(book)
    await db.commit()
    await db.refresh(book)
    return book

更新数据:

python 复制代码
@app.put("/book/{book_id}")
async def update_book(
    book_id: int,
    data: BookCreate,
    db: AsyncSession = Depends(get_db)
):
    book = await db.get(Book, book_id)
    if book is None:
        raise HTTPException(status_code=404, detail="图书不存在")

    book.bookname = data.bookname
    book.author = data.author
    book.price = data.price
    book.publisher = data.publisher

    await db.commit()
    return book

删除数据:

python 复制代码
@app.delete("/book/{book_id}")
async def delete_book(book_id: int, db: AsyncSession = Depends(get_db)):
    book = await db.get(Book, book_id)
    if book is None:
        raise HTTPException(status_code=404, detail="图书不存在")

    await db.delete(book)
    await db.commit()
    return {"message": "删除成功"}

更新和删除都建议先查。

不是为了多写几行代码,而是为了让接口语义清楚。

找不到目标资源,就应该明确返回 404。

10. 事务边界要统一

数据库写操作离不开事务。

常见规则:

操作 是否需要 commit
查询 通常不需要
新增 需要
更新 需要
删除 需要
异常 应该 rollback

课程里有些代码会在依赖项里提交,也会在路由函数里提交。

学习时能理解即可,但文章里的示例必须统一。

本文统一采用"写操作显式提交"的方式。

也就是说,get_db() 不在 yield 后自动 commit(),新增、更新、删除接口自己调用 await db.commit()

团队如果选择依赖统一提交,也可以,但那就应该删除路由和业务函数里的手动 commit()

关键是别混。

事务边界混乱,后面排查数据问题会很痛苦。

11. 在 AI 掘金头条项目里的完整链路

新闻列表接口大概是:

text 复制代码
router 接收 page/categoryId
-> crud 构造 select
-> AsyncSession 执行 SQL
-> MySQL 返回新闻列表
-> schema 转响应结构
-> 返回给前端

画出来:
#mermaid-svg-IqhV1YaVskifnf27{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IqhV1YaVskifnf27 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IqhV1YaVskifnf27 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IqhV1YaVskifnf27 .error-icon{fill:#552222;}#mermaid-svg-IqhV1YaVskifnf27 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IqhV1YaVskifnf27 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IqhV1YaVskifnf27 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IqhV1YaVskifnf27 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IqhV1YaVskifnf27 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IqhV1YaVskifnf27 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IqhV1YaVskifnf27 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IqhV1YaVskifnf27 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IqhV1YaVskifnf27 .marker.cross{stroke:#333333;}#mermaid-svg-IqhV1YaVskifnf27 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IqhV1YaVskifnf27 p{margin:0;}#mermaid-svg-IqhV1YaVskifnf27 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IqhV1YaVskifnf27 .cluster-label text{fill:#333;}#mermaid-svg-IqhV1YaVskifnf27 .cluster-label span{color:#333;}#mermaid-svg-IqhV1YaVskifnf27 .cluster-label span p{background-color:transparent;}#mermaid-svg-IqhV1YaVskifnf27 .label text,#mermaid-svg-IqhV1YaVskifnf27 span{fill:#333;color:#333;}#mermaid-svg-IqhV1YaVskifnf27 .node rect,#mermaid-svg-IqhV1YaVskifnf27 .node circle,#mermaid-svg-IqhV1YaVskifnf27 .node ellipse,#mermaid-svg-IqhV1YaVskifnf27 .node polygon,#mermaid-svg-IqhV1YaVskifnf27 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IqhV1YaVskifnf27 .rough-node .label text,#mermaid-svg-IqhV1YaVskifnf27 .node .label text,#mermaid-svg-IqhV1YaVskifnf27 .image-shape .label,#mermaid-svg-IqhV1YaVskifnf27 .icon-shape .label{text-anchor:middle;}#mermaid-svg-IqhV1YaVskifnf27 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IqhV1YaVskifnf27 .rough-node .label,#mermaid-svg-IqhV1YaVskifnf27 .node .label,#mermaid-svg-IqhV1YaVskifnf27 .image-shape .label,#mermaid-svg-IqhV1YaVskifnf27 .icon-shape .label{text-align:center;}#mermaid-svg-IqhV1YaVskifnf27 .node.clickable{cursor:pointer;}#mermaid-svg-IqhV1YaVskifnf27 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IqhV1YaVskifnf27 .arrowheadPath{fill:#333333;}#mermaid-svg-IqhV1YaVskifnf27 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IqhV1YaVskifnf27 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IqhV1YaVskifnf27 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IqhV1YaVskifnf27 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IqhV1YaVskifnf27 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IqhV1YaVskifnf27 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IqhV1YaVskifnf27 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IqhV1YaVskifnf27 .cluster text{fill:#333;}#mermaid-svg-IqhV1YaVskifnf27 .cluster span{color:#333;}#mermaid-svg-IqhV1YaVskifnf27 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IqhV1YaVskifnf27 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IqhV1YaVskifnf27 rect.text{fill:none;stroke-width:0;}#mermaid-svg-IqhV1YaVskifnf27 .icon-shape,#mermaid-svg-IqhV1YaVskifnf27 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IqhV1YaVskifnf27 .icon-shape p,#mermaid-svg-IqhV1YaVskifnf27 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IqhV1YaVskifnf27 .icon-shape .label rect,#mermaid-svg-IqhV1YaVskifnf27 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IqhV1YaVskifnf27 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IqhV1YaVskifnf27 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IqhV1YaVskifnf27 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 前端请求 /api/news/list
router/news.py
crud/news.py
select(News).where(...).offset().limit()
AsyncSession
MySQL news 表
ORM 新闻对象
Pydantic 响应模型
JSON 返回前端

这就是 FastAPI 项目里数据库访问最常见的形态。

12. 小结

FastAPI + SQLAlchemy Async 不要拆成一堆孤立 API 记。

按这条线记:

text 复制代码
create_async_engine
-> async_sessionmaker
-> Depends(get_db)
-> AsyncSession
-> select/add/delete
-> commit/rollback/close

ORM 模型负责表结构,Session 负责一次数据库工作,事务负责最终是否落库。

下一篇我们把这些能力放到完整项目里看,看看一个新闻项目为什么要分 routercrudmodelschema

参考资料