Skip to content

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 条判断是否还有下一页。响应模型中的排除参数适合少量特殊需求,长期接口更推荐定义清晰独立的输出模型。

练习:分别验证无匹配条件、最后一页、超出最后一页;尝试移除排序并解释为何分页结果可能变化。