Skip to content

17. FastAPI 响应类型_分页列表封装 ​

将重复分页结构提炼成泛型 ​

用户列表、商品列表都需要 items、total、页码等字段;变化的只是列表项类型。泛型模型可以像模具一样复用:Page[UserOut] 装用户,Page[ProductOut] 装商品。

完整示例 ​

python
from typing import Annotated, Generic, TypeVar
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field

app = FastAPI()
T = TypeVar("T")

class Page(BaseModel, Generic[T]):
    items: list[T]
    total: int = Field(ge=0)
    page: int = Field(ge=1)
    size: int = Field(ge=1)
    pages: int = Field(ge=0)
    has_next: bool

class UserOut(BaseModel):
    id: int
    name: str

USERS = [{"id": i, "name": f"用户{i}"} for i in range(1, 24)]

def make_page(items: list[T], total: int, page: int, size: int) -> Page[T]:
    return Page(items=items, total=total, page=page, size=size,
                pages=(total + size - 1) // size,
                has_next=page * size < total)

@app.get("/users", response_model=Page[UserOut])
def users(
    page: Annotated[int, Query(ge=1)] = 1,
    size: Annotated[int, Query(ge=1, le=100)] = 10,
):
    offset = (page - 1) * size
    items = [UserOut.model_validate(x) for x in USERS[offset:offset + size]]
    return make_page(items, len(USERS), page, size)

保存为 main.py,启动 uv run uvicorn main:app --reload。/users?page=3&size=10 返回 3 条记录、total=23、pages=3、has_next=false。空集合总页数约定为 0;请求第 1 页仍合法。约定需要在所有列表接口保持一致。

分页参数与响应模型分开 ​

输入参数负责限制页码和大小;输出模型负责约定结果结构;make_page 只计算元信息,不负责查询数据库。这种划分让数据库查询、内存数据、外部服务都可以使用同一响应结构。

接入 SQLAlchemy 时,在同一业务方法中构造 COUNT 和当前页查询,共用筛选条件。涉及一对多 JOIN 时,统计 JOIN 行数可能重复计算父记录,需要 count(distinct(...))、子查询或先选父 ID。

一致性与演进 ​

总量查询与分页查询是两次 SQL,并发写入时可能看到不同数据。是否需要相同快照取决于事务隔离级别和业务要求;不能因为写在同一个 Python 函数里就宣称完全一致。

泛型不要求使用已过时的 pydantic.generics.GenericModel;本系列采用 Pydantic 2 的 BaseModel + Generic。若想改用游标分页,应另建 CursorPage,包含 next_cursor、has_next,不要强行伪造页码和总页数。

练习:创建 ProductOut 并复用 Page[ProductOut];确认 /docs 里两个接口的列表元素模型各自正确。