skillhub-061-api-designer
495 行
9.5 KiB
Markdown
495 行
9.5 KiB
Markdown
# 分页模式
|
||
|
||
## 为什么需要分页?
|
||
|
||
大型集合不能一次性全部返回,原因在于:
|
||
- 性能问题(查询速度慢、数据载荷大)
|
||
- 内存限制(服务端和客户端)
|
||
- 网络超时
|
||
- 用户体验差
|
||
|
||
集合端点始终要进行分页。
|
||
|
||
## 分页策略
|
||
|
||
### 1. 基于偏移量的分页(Offset-Based Pagination)
|
||
|
||
最常见且直观。使用 `offset`(跳过数量)和 `limit`(每页条数)。
|
||
|
||
**请求:**
|
||
```http
|
||
GET /users?offset=20&limit=10
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"data": [
|
||
{"id": 21, "name": "User 21"},
|
||
{"id": 22, "name": "User 22"}
|
||
],
|
||
"pagination": {
|
||
"offset": 20,
|
||
"limit": 10,
|
||
"total": 150,
|
||
"has_more": true
|
||
},
|
||
"links": {
|
||
"first": "/users?offset=0&limit=10",
|
||
"prev": "/users?offset=10&limit=10",
|
||
"next": "/users?offset=30&limit=10",
|
||
"last": "/users?offset=140&limit=10"
|
||
}
|
||
}
|
||
```
|
||
|
||
**优点:**
|
||
- 实现简单
|
||
- 易于理解
|
||
- 支持随机访问(可跳转到任意页)
|
||
- 显示总条数
|
||
|
||
**缺点:**
|
||
- 偏移量越大性能越差(数据库需要扫描大量行)
|
||
- 分页过程中数据发生变化会导致结果不一致
|
||
- 对实时数据效率低下
|
||
- 数据库必须计算总行数(开销大)
|
||
|
||
**适用场景:**
|
||
- 中小型数据集
|
||
- 数据变更不频繁
|
||
- 需要随机访问页面
|
||
- 需要总条数
|
||
|
||
### 2. 基于页码的分页(Page-Based Pagination)
|
||
|
||
使用页码简化的偏移量分页。
|
||
|
||
**请求:**
|
||
```http
|
||
GET /users?page=3&per_page=10
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"data": [...],
|
||
"pagination": {
|
||
"page": 3,
|
||
"per_page": 10,
|
||
"total_pages": 15,
|
||
"total_count": 150
|
||
},
|
||
"links": {
|
||
"first": "/users?page=1&per_page=10",
|
||
"prev": "/users?page=2&per_page=10",
|
||
"next": "/users?page=4&per_page=10",
|
||
"last": "/users?page=15&per_page=10"
|
||
}
|
||
}
|
||
```
|
||
|
||
**计算公式:**
|
||
- `offset = (page - 1) * per_page`
|
||
- `total_pages = ceil(total_count / per_page)`
|
||
|
||
**优缺点与基于偏移量的分页相同,但:**
|
||
- 对用户更直观(第 1 页、第 2 页)
|
||
- 在 Web 应用中常见
|
||
|
||
### 3. 基于游标的分页(Cursor-Based Pagination)
|
||
|
||
使用一个不透明的游标(指针)指向下一组结果。
|
||
|
||
**请求:**
|
||
```http
|
||
GET /users?limit=10
|
||
GET /users?cursor=eyJpZCI6MTIzfQ&limit=10
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"data": [
|
||
{"id": 21, "name": "User 21"},
|
||
{"id": 22, "name": "User 22"}
|
||
],
|
||
"pagination": {
|
||
"next_cursor": "eyJpZCI6MzB9",
|
||
"prev_cursor": "eyJpZCI6MjB9",
|
||
"has_more": true
|
||
},
|
||
"links": {
|
||
"next": "/users?cursor=eyJpZCI6MzB9&limit=10",
|
||
"prev": "/users?cursor=eyJpZCI6MjB9&limit=10"
|
||
}
|
||
}
|
||
```
|
||
|
||
**游标结构(base64 编码):**
|
||
```json
|
||
{"id": 30, "sort": "created_at"}
|
||
```
|
||
|
||
**实现方式:**
|
||
```sql
|
||
-- 第一页
|
||
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
|
||
|
||
-- 下一页(游标指向最后一项)
|
||
SELECT * FROM users
|
||
WHERE created_at < '2024-01-15T10:30:00Z'
|
||
ORDER BY created_at DESC
|
||
LIMIT 10;
|
||
```
|
||
|
||
**优点:**
|
||
- 结果一致(不会出现跳过或重复项)
|
||
- 对大型数据集效率高
|
||
- 适用于实时数据
|
||
- 无需昂贵的 COUNT 查询
|
||
- 数据库性能更优
|
||
|
||
**缺点:**
|
||
- 不支持随机访问(无法跳转到第 10 页)
|
||
- 不提供总条数
|
||
- 实现较复杂
|
||
- 游标不透明(用户无法修改)
|
||
|
||
**适用场景:**
|
||
- 大型数据集
|
||
- 数据频繁变更
|
||
- 无限滚动 UI
|
||
- 实时信息流
|
||
- 性能至关重要
|
||
|
||
### 4. 键集分页(Keyset Pagination)
|
||
|
||
与游标分页类似,但使用实际的字段值而非不透明的游标。
|
||
|
||
**请求:**
|
||
```http
|
||
GET /users?after_id=20&limit=10
|
||
GET /users?after_created_at=2024-01-15T10:30:00Z&limit=10
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"data": [
|
||
{"id": 21, "name": "User 21", "created_at": "2024-01-15T11:00:00Z"},
|
||
{"id": 22, "name": "User 22", "created_at": "2024-01-15T11:30:00Z"}
|
||
],
|
||
"pagination": {
|
||
"after_id": 30,
|
||
"limit": 10,
|
||
"has_more": true
|
||
},
|
||
"links": {
|
||
"next": "/users?after_id=30&limit=10"
|
||
}
|
||
}
|
||
```
|
||
|
||
**实现方式:**
|
||
```sql
|
||
SELECT * FROM users
|
||
WHERE id > 20
|
||
ORDER BY id ASC
|
||
LIMIT 10;
|
||
```
|
||
|
||
**优点:**
|
||
- 非常高效(利用索引)
|
||
- 游标透明(人类可读)
|
||
- 结果一致
|
||
- 实现简单
|
||
|
||
**缺点:**
|
||
- 需要已建立索引的列
|
||
- 不支持随机访问
|
||
- 排序仅限于游标字段
|
||
- 多字段排序时较复杂
|
||
|
||
**适用场景:**
|
||
- 简单排序(按 ID、时间戳)
|
||
- 需要高效分页
|
||
- 希望游标透明
|
||
- 已有合适的索引
|
||
|
||
### 5. 查找分页/基于时间的分页(Seek Pagination / Time-Based)
|
||
|
||
针对时序数据专用的键集分页。
|
||
|
||
**请求:**
|
||
```http
|
||
GET /events?since=2024-01-15T10:00:00Z&until=2024-01-15T11:00:00Z&limit=100
|
||
```
|
||
|
||
**响应:**
|
||
```json
|
||
{
|
||
"data": [...],
|
||
"pagination": {
|
||
"since": "2024-01-15T10:00:00Z",
|
||
"until": "2024-01-15T11:00:00Z",
|
||
"limit": 100,
|
||
"has_more": true
|
||
},
|
||
"links": {
|
||
"next": "/events?since=2024-01-15T11:00:00Z&until=2024-01-15T12:00:00Z&limit=100"
|
||
}
|
||
}
|
||
```
|
||
|
||
**适用场景:**
|
||
- 时序数据
|
||
- 日志和事件
|
||
- 活动流
|
||
- 分析数据
|
||
|
||
## 默认限制
|
||
|
||
始终设置合理的默认值和最大限制:
|
||
|
||
```json
|
||
{
|
||
"default_limit": 20,
|
||
"max_limit": 100,
|
||
"min_limit": 1
|
||
}
|
||
```
|
||
|
||
**校验:**
|
||
```http
|
||
GET /users?limit=1000
|
||
|
||
响应:400 Bad Request
|
||
{
|
||
"error": {
|
||
"code": "INVALID_LIMIT",
|
||
"message": "Limit must be between 1 and 100. Default is 20."
|
||
}
|
||
}
|
||
```
|
||
|
||
## 响应格式
|
||
|
||
### 标准分页对象
|
||
|
||
```json
|
||
{
|
||
"data": [...],
|
||
"pagination": {
|
||
"limit": 10,
|
||
"offset": 20,
|
||
"total": 150,
|
||
"has_more": true,
|
||
"has_previous": true
|
||
}
|
||
}
|
||
```
|
||
|
||
### 链接头部(RFC 5988)
|
||
|
||
```http
|
||
Link: </users?offset=0&limit=10>; rel="first",
|
||
</users?offset=10&limit=10>; rel="prev",
|
||
</users?offset=30&limit=10>; rel="next",
|
||
</users?offset=140&limit=10>; rel="last"
|
||
```
|
||
|
||
**使用方:** GitHub API
|
||
|
||
### 嵌入式链接
|
||
|
||
```json
|
||
{
|
||
"data": [...],
|
||
"_links": {
|
||
"self": { "href": "/users?offset=20&limit=10" },
|
||
"first": { "href": "/users?offset=0&limit=10" },
|
||
"prev": { "href": "/users?offset=10&limit=10" },
|
||
"next": { "href": "/users?offset=30&limit=10" },
|
||
"last": { "href": "/users?offset=140&limit=10" }
|
||
}
|
||
}
|
||
```
|
||
|
||
## 分页与排序
|
||
|
||
分页时始终支持排序:
|
||
|
||
```http
|
||
GET /users?sort=created_at&order=desc&limit=10
|
||
GET /users?sort=-created_at&limit=10 # 降序
|
||
GET /users?sort=last_name,first_name&limit=10 # 多字段
|
||
```
|
||
|
||
**对于游标分页,游标必须包含排序字段:**
|
||
```json
|
||
{
|
||
"cursor": {
|
||
"id": 123,
|
||
"created_at": "2024-01-15T10:30:00Z",
|
||
"sort_fields": ["created_at", "id"]
|
||
}
|
||
}
|
||
```
|
||
|
||
## 分页与过滤
|
||
|
||
将过滤与分页结合使用:
|
||
|
||
```http
|
||
GET /users?status=active&role=admin&offset=0&limit=10
|
||
```
|
||
|
||
**重要提示:** 先过滤再分页:
|
||
1. 过滤记录
|
||
2. 统计过滤后的结果数
|
||
3. 应用分页
|
||
4. 返回分页后的子集
|
||
|
||
## 总条数
|
||
|
||
### 包含总条数
|
||
|
||
```json
|
||
{
|
||
"data": [...],
|
||
"pagination": {
|
||
"total": 1523,
|
||
"limit": 10,
|
||
"offset": 20
|
||
}
|
||
}
|
||
```
|
||
|
||
**优点:**
|
||
- 客户端知道结果总数
|
||
- 可计算总页数
|
||
- 更好的用户体验(显示"第 3 页,共 153 页")
|
||
|
||
**缺点:**
|
||
- COUNT 查询开销大
|
||
- 拖慢响应速度
|
||
- 对大型或频繁变更的数据集不准确
|
||
|
||
### 省略总条数
|
||
|
||
```json
|
||
{
|
||
"data": [...],
|
||
"pagination": {
|
||
"has_more": true,
|
||
"limit": 10
|
||
}
|
||
}
|
||
```
|
||
|
||
**适用场景:**
|
||
- 大型数据集(COUNT 太慢)
|
||
- 实时数据(总数不断变化)
|
||
- 游标分页
|
||
- 无限滚动 UI
|
||
|
||
### 可选的总条数
|
||
|
||
让客户端决定是否请求总条数:
|
||
|
||
```http
|
||
GET /users?limit=10&include_total=true
|
||
```
|
||
|
||
## 边界情况
|
||
|
||
### 空结果
|
||
|
||
```json
|
||
{
|
||
"data": [],
|
||
"pagination": {
|
||
"offset": 0,
|
||
"limit": 10,
|
||
"total": 0,
|
||
"has_more": false
|
||
}
|
||
}
|
||
```
|
||
|
||
### 最后一页
|
||
|
||
```json
|
||
{
|
||
"data": [{"id": 150, "name": "Last User"}],
|
||
"pagination": {
|
||
"offset": 140,
|
||
"limit": 10,
|
||
"total": 150,
|
||
"has_more": false
|
||
},
|
||
"links": {
|
||
"first": "/users?offset=0&limit=10",
|
||
"prev": "/users?offset=130&limit=10",
|
||
"next": null
|
||
}
|
||
}
|
||
```
|
||
|
||
### 超出范围
|
||
|
||
```http
|
||
GET /users?offset=10000&limit=10
|
||
|
||
响应:200 OK(空结果)
|
||
{
|
||
"data": [],
|
||
"pagination": {
|
||
"offset": 10000,
|
||
"limit": 10,
|
||
"total": 150,
|
||
"has_more": false
|
||
}
|
||
}
|
||
```
|
||
|
||
或者对于不存在的页面返回 404:
|
||
```http
|
||
GET /users?page=1000&per_page=10
|
||
|
||
响应:404 Not Found
|
||
{
|
||
"error": {
|
||
"code": "PAGE_NOT_FOUND",
|
||
"message": "Page 1000 does not exist. Total pages: 15"
|
||
}
|
||
}
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
1. **始终对集合进行分页**——永远不要返回无限制的列表
|
||
2. **设置合理的默认值**——默认每页 20–50 条
|
||
3. **强制执行最大限制**——防止过载(最大 100–1000 条)
|
||
4. **包含 has_more 标志**——告知客户端是否还有更多结果
|
||
5. **提供导航链接**——让获取上一页/下一页变得简单
|
||
6. **文档化分页**——说明游标格式、限制、默认值
|
||
7. **保持一致性**——在所有端点上使用相同的分页模式
|
||
8. **考虑性能**——根据数据大小/类型选择策略
|
||
9. **支持排序**——让客户端控制结果顺序
|
||
10. **处理边界情况**——空结果、最后一页、无效游标
|
||
|
||
## 对比矩阵
|
||
|
||
| 特性 | 偏移量 | 页码 | 游标 | 键集 |
|
||
|---------|--------|------|--------|--------|
|
||
| 性能 | 偏移量大时差 | 差 | 优秀 | 优秀 |
|
||
| 随机访问 | 支持 | 支持 | 不支持 | 不支持 |
|
||
| 总条数 | 支持 | 支持 | 不支持 | 可选 |
|
||
| 一致性 | 差 | 差 | 优秀 | 优秀 |
|
||
| 复杂度 | 简单 | 简单 | 中等 | 中等 |
|
||
| 实时数据 | 差 | 差 | 优秀 | 优秀 |
|
||
| 数据库负载 | 高 | 高 | 低 | 低 |
|
||
| 适用场景 | 小数据集 | Web UI | 信息流/流式数据 | 大型数据集 |
|