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
331 lines
6.8 KiB
Markdown
331 lines
6.8 KiB
Markdown
# 分页使用指南
|
|
|
|
## 📋 概述
|
|
|
|
小虾 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 (
|
|
<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 示例
|
|
|
|
```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. 合理的页面大小
|
|
|
|
```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
|