学 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是主键bookname、author、price、publisher是字段
SQLAlchemy 2.0 推荐使用 Mapped 和 mapped_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 负责一次数据库工作,事务负责最终是否落库。
下一篇我们把这些能力放到完整项目里看,看看一个新闻项目为什么要分 router、crud、model、schema。