# 分页使用指南 ## 📋 概述 小虾 SaaS 提供统一的分页接口,支持内存分页和数据库分页。 --- ## 🔧 基本使用 ### 1. API 端点添加分页 ```python from fastapi import APIRouter, Depends from packages.application.common.pagination import PaginationParams, PaginatedResponse router = APIRouter() @router.get("/projects", response_model=PaginatedResponse[ProjectResponse]) async def list_projects( pagination: PaginationParams = Depends(), user_id: str = Depends(get_current_user_id), ): # 获取分页数据 projects, total = project_repo.find_by_user_paginated( user_id=user_id, offset=pagination.offset, limit=pagination.limit, ) # 返回分页响应 return PaginatedResponse.create( data=projects, params=pagination, total=total, ) ``` ### 2. 请求分页数据 ```bash # 默认第一页,每页 20 条 GET /api/v1/projects # 指定页码和每页数量 GET /api/v1/projects?page=2&page_size=10 # 获取更多数据 GET /api/v1/projects?page=1&page_size=50 ``` ### 3. 响应格式 ```json { "data": [ { "id": "project-1", "name": "项目 1" }, { "id": "project-2", "name": "项目 2" } ], "pagination": { "page": 1, "page_size": 20, "total": 156, "total_pages": 8, "has_next": true, "has_prev": false } } ``` --- ## 🗄️ Repository 实现 ### InMemory Repository(内存分页) ```python from packages.application.common.pagination import paginate, PaginationParams class InMemoryProjectRepository: def find_by_workspace_paginated( self, workspace_id: str, pagination: PaginationParams, ): # 获取所有项目 all_projects = [p for p in self._storage.values() if p.workspace_id == workspace_id] # 使用内存分页 return paginate(all_projects, pagination) ``` ### PostgreSQL Repository(数据库分页) ```python class PostgresProjectRepository: def find_by_workspace_paginated( self, workspace_id: str, offset: int, limit: int, ) -> tuple[List[Project], int]: conn = self._get_connection() try: with conn.cursor() as cur: # 获取总数 cur.execute( "SELECT COUNT(*) FROM projects WHERE workspace_id = %s", (workspace_id,) ) total = cur.fetchone()["count"] # 获取分页数据 cur.execute(""" SELECT * FROM projects WHERE workspace_id = %s ORDER BY created_at DESC LIMIT %s OFFSET %s """, (workspace_id, limit, offset)) projects = [self._row_to_project(row) for row in cur.fetchall()] return projects, total finally: conn.close() ``` --- ## 📱 前端使用 ### React 示例 ```typescript import { useState, useEffect } from 'react'; function ProjectList() { const [projects, setProjects] = useState([]); const [pagination, setPagination] = useState(null); const [page, setPage] = useState(1); useEffect(() => { fetch(`/api/v1/projects?page=${page}&page_size=20`) .then(res => res.json()) .then(data => { setProjects(data.data); setPagination(data.pagination); }); }, [page]); return (
); } ``` ### Vue 示例 ```vue ``` --- ## 🎯 最佳实践 ### 1. 合理的页面大小 ```python # ✅ 推荐 page_size: int = Field(20, ge=1, le=100) # 默认 20,最大 100 # ❌ 不推荐 page_size: int = Field(1000, ge=1, le=10000) # 太大,性能差 ``` ### 2. 默认排序 ```python # ✅ 总是指定排序 ORDER BY created_at DESC # ❌ 不指定排序(结果不稳定) SELECT * FROM projects LIMIT 20 ``` ### 3. 使用游标分页(大数据量) ```python # 基于 ID 的游标分页(更快) @router.get("/projects") async def list_projects( cursor: Optional[str] = None, # 上一页最后一个 ID limit: int = 20, ): if cursor: projects = project_repo.find_after_cursor(cursor, limit) else: projects = project_repo.find_first_page(limit) return { "data": projects, "next_cursor": projects[-1].id if projects else None, "has_more": len(projects) == limit } ``` ### 4. 缓存总数(可选) ```python # 对于不常变化的列表,缓存总数 from functools import lru_cache @lru_cache(maxsize=128) def get_total_projects(workspace_id: str) -> int: return project_repo.count_by_workspace(workspace_id) ``` --- ## 📊 性能对比 ### Offset/Limit 分页 **优点:** - 简单易用 - 支持跳转到任意页 **缺点:** - 深分页慢(OFFSET 10000 需要扫描 10000 行) - 数据变化时可能重复/遗漏 **适用场景:** - 数据量小(< 10,000 条) - 用户很少翻到后面 ### 游标分页 **优点:** - 性能稳定(始终快速) - 不受数据变化影响 **缺点:** - 不支持跳转 - 只能上一页/下一页 **适用场景:** - 数据量大(> 100,000 条) - 无限滚动 --- ## 🔗 相关资源 - [PostgreSQL LIMIT/OFFSET 优化](https://www.postgresql.org/docs/current/queries-limit.html) - [游标分页最佳实践](https://use-the-index-luke.com/sql/partial-results/fetch-next-page) - [GraphQL Cursor Connections](https://relay.dev/graphql/connections.htm) --- **最后更新:** 2026-06-17