a67f1b2a0f
- Implement PaginationParams with offset/limit calculation - Add PaginationMeta with navigation metadata - Create generic PaginatedResponse[T] with type safety - Support both in-memory and database pagination - Include has_next/has_prev navigation flags - Add comprehensive pagination documentation - Include frontend integration examples (React/Vue) - Cover cursor pagination for large datasets Phase 4 Task 47/68 completed
6.8 KiB
6.8 KiB
分页使用指南
📋 概述
小虾 SaaS 提供统一的分页接口,支持内存分页和数据库分页。
🔧 基本使用
1. API 端点添加分页
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. 请求分页数据
# 默认第一页,每页 20 条
GET /api/v1/projects
# 指定页码和每页数量
GET /api/v1/projects?page=2&page_size=10
# 获取更多数据
GET /api/v1/projects?page=1&page_size=50
3. 响应格式
{
"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(内存分页)
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(数据库分页)
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 示例
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 (
<div>
<ul>
{projects.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
<Pagination
current={pagination?.page}
total={pagination?.total}
pageSize={pagination?.page_size}
onChange={setPage}
showPrevNext={true}
disabled={{
prev: !pagination?.has_prev,
next: !pagination?.has_next
}}
/>
</div>
);
}
Vue 示例
<template>
<div>
<ul>
<li v-for="project in projects" :key="project.id">
{{ project.name }}
</li>
</ul>
<el-pagination
:current-page="pagination.page"
:page-size="pagination.page_size"
:total="pagination.total"
@current-change="handlePageChange"
/>
</div>
</template>
<script>
export default {
data() {
return {
projects: [],
pagination: {
page: 1,
page_size: 20,
total: 0
}
};
},
methods: {
async fetchProjects(page = 1) {
const response = await fetch(
`/api/v1/projects?page=${page}&page_size=20`
);
const data = await response.json();
this.projects = data.data;
this.pagination = data.pagination;
},
handlePageChange(page) {
this.fetchProjects(page);
}
},
mounted() {
this.fetchProjects();
}
};
</script>
🎯 最佳实践
1. 合理的页面大小
# ✅ 推荐
page_size: int = Field(20, ge=1, le=100) # 默认 20,最大 100
# ❌ 不推荐
page_size: int = Field(1000, ge=1, le=10000) # 太大,性能差
2. 默认排序
# ✅ 总是指定排序
ORDER BY created_at DESC
# ❌ 不指定排序(结果不稳定)
SELECT * FROM projects LIMIT 20
3. 使用游标分页(大数据量)
# 基于 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. 缓存总数(可选)
# 对于不常变化的列表,缓存总数
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 条)
- 无限滚动
🔗 相关资源
最后更新: 2026-06-17