16. FastAPI 响应类型_数据过滤与分页
“过滤”有两个不同方向
数据筛选决定返回哪些记录,例如只查在售商品;字段过滤决定每条记录公开哪些字段,例如隐藏进货价。前者通过查询条件完成,后者通过响应模型完成。分页再决定这次取符合条件的哪一段。
可以直接运行的分页接口
python
from typing import Annotated
from fastapi import FastAPI, Query
from pydantic import BaseModel
app = FastAPI()
DATA = [
{"id": 1, "name": "键盘", "active": True, "cost": 80},
{"id": 2, "name": "鼠标", "active": False, "cost": 30},
{"id": 3, "name": "显示器", "active": True, "cost": 600},
]
class ItemOut(BaseModel):
id: int
name: str
class ItemPage(BaseModel):
items: list[ItemOut]
total: int
page: int
size: int
@app.get("/items", response_model=ItemPage)
def list_items(
page: Annotated[int, Query(ge=1)] = 1,
size: Annotated[int, Query(ge=1, le=100)] = 2,
keyword: str = "",
active: bool | None = None,
):
rows = [x for x in DATA
if keyword in x["name"]
and (active is None or x["active"] == active)]
rows.sort(key=lambda x: x["id"])
total = len(rows)
offset = (page - 1) * size
return {"items": rows[offset:offset + size],
"total": total, "page": page, "size": size}保存为 main.py,运行 uv run uvicorn main:app --reload。/items?active=true&size=1&page=2 返回显示器,total=2,且没有 cost 字段。页码超出末页通常返回空列表;页码为零属于参数错误。
正确的执行顺序
先筛选 → 按稳定规则排序 → 统计总量 → 截取当前页 → 转换公开模型。如果先分页再筛选,会出现本来有数据却显示空页的问题;如果 total 不使用同一筛选条件,页数会算错。
上面的内存列表适合教学。数据库项目应将筛选、排序、offset、limit 下推到 SQL,不能先 .all() 再切片。
大表为什么需要游标分页
偏移分页易理解,可跳转页码,但深分页可能扫描并丢弃大量记录。游标分页形如 WHERE id > :last_id ORDER BY id LIMIT :size,适合连续浏览。
按时间排序时,加主键作为第二排序键,如 (created_at, id),游标也要包含两者。只有 ORDER BY created_at 时,时间相同的记录顺序可能不稳定。并发新增和删除还会影响页边界;需要强一致视图时必须另行设计快照或时间边界。
总量统计可能比列表查询更贵。允许时只返回 has_next,取 size+1 条判断是否还有下一页。响应模型中的排除参数适合少量特殊需求,长期接口更推荐定义清晰独立的输出模型。
练习:分别验证无匹配条件、最后一页、超出最后一页;尝试移除排序并解释为何分页结果可能变化。