vault backup: 2026-04-28 20:56:51
This commit is contained in:
+74
-26
@@ -136,25 +136,32 @@ totalPages := int(math.Ceil(float64(total) / float64(perPage)))
|
||||
传统的 `LIMIT/OFFSET` 分页在深页性能急剧下降。游标分页通过「记住上一页最后一条记录的位置」来实现 O(log n) 的跳转:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID uint
|
||||
Name string
|
||||
Age int
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// 第一页:没有 cursor,直接取前 N 条
|
||||
cursor := "" // 空表示从头开始
|
||||
cursor := uint(0) // 用主键类型更严谨
|
||||
perPage := 20
|
||||
|
||||
var users []User
|
||||
query := db.Model(&User{}).Order("id ASC").Limit(perPage + 1) // 多取 1 条判断是否有下一页
|
||||
q := db.Model(&User{}).Order("id ASC").Limit(perPage + 1) // 多取 1 条判断是否有下一页
|
||||
|
||||
if cursor != "" {
|
||||
if cursor != 0 {
|
||||
// 查找 id > cursor 的记录
|
||||
query = query.Where("id > ?", cursor)
|
||||
q = q.Where("id > ?", cursor)
|
||||
}
|
||||
|
||||
err := query.Find(&users).Error
|
||||
err := q.Find(&users).Error
|
||||
hasNextPage := false
|
||||
|
||||
if len(users) > perPage {
|
||||
hasNextPage = true
|
||||
users = users[:perPage] // 去掉多余的「探测」记录
|
||||
cursor = fmt.Sprint(users[len(users)-1].ID) // 提取新的 cursor
|
||||
users = users[:perPage] // 去掉多余的「探测」记录
|
||||
cursor = users[len(users)-1].ID // 提取新的 cursor
|
||||
}
|
||||
```
|
||||
|
||||
@@ -177,7 +184,7 @@ type PageResult struct {
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
func Paginate[T any](db *gorm.DB, where any, order string, page, pageSize int) (*PageResult, error) {
|
||||
func Paginate[T any](db *gorm.DB, where any, args ...any, order string, page, pageSize int) (*PageResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
@@ -188,7 +195,7 @@ func Paginate[T any](db *gorm.DB, where any, order string, page, pageSize int) (
|
||||
var total int64
|
||||
q := db.Model(new(T))
|
||||
if where != nil {
|
||||
q = q.Where(where)
|
||||
q = q.Where(where, args...)
|
||||
}
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -196,7 +203,7 @@ func Paginate[T any](db *gorm.DB, where any, order string, page, pageSize int) (
|
||||
|
||||
var items []T
|
||||
offset := (page - 1) * pageSize
|
||||
q.Order(order)
|
||||
q = q.Order(order)
|
||||
if err := q.Limit(pageSize).Offset(offset).Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -209,10 +216,49 @@ func Paginate[T any](db *gorm.DB, where any, order string, page, pageSize int) (
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
// 无过滤条件
|
||||
result, err := Paginate[User](db, nil, "created_at DESC", page, 20)
|
||||
|
||||
// 参数化条件查询
|
||||
result, err := Paginate[User](db, "status = ?", "active", "created_at DESC", page, 20)
|
||||
|
||||
// 结构体条件(GORM 自动处理字段名)
|
||||
result, err := Paginate[User](db, User{Status: "active"}, "created_at DESC", page, 20)
|
||||
```
|
||||
|
||||
## HTTP Handler 中的分页
|
||||
|
||||
在实际项目中,分页参数通常来自 URL query string。以 Gin 框架为例:
|
||||
|
||||
```go
|
||||
func ListUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
// 白名单校验排序字段
|
||||
order := "created_at DESC"
|
||||
if field := c.Query("order_by"); field != "" {
|
||||
if allowedOrder[field] {
|
||||
order = field + " DESC"
|
||||
}
|
||||
}
|
||||
|
||||
result, err := Paginate[User](c.MustGet("db").(*gorm.DB), nil, order, page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
// GET /users?page=2&page_size=10&order_by=name
|
||||
```
|
||||
|
||||
> [!tip] 前端推荐参数名
|
||||
> - `page`:页码(从 1 开始)
|
||||
> - `page_size`:每页条数
|
||||
> - `order_by`:排序字段(配合后端白名单)
|
||||
|
||||
## DefaultPageSize 配置
|
||||
|
||||
GORM 提供了 `DefaultPageSize` 配置项作为全局兜底:
|
||||
@@ -235,22 +281,24 @@ db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[需要分页/排序数据] --> SortQ{需要自定义排序_}
|
||||
SortQ --> |否| DefaultSort["ORDER BY 主键 ASC<br/>GORM 默认排序"]
|
||||
SortQ --> |是| Whitelist{"字段在白名单中?"}
|
||||
Whitelist --> |是| CustomSort["db.Order(field + direction)"]
|
||||
Whitelist --> |否| DefaultSort
|
||||
|
||||
DefaultSort --> PagesQ{要跳页还是滚动?}
|
||||
Start["开始:需要分页/排序数据"] --> SortQ{"需要自定义排序?"}
|
||||
|
||||
SortQ -->|"否"| DefaultSort["ORDER BY 主键 ASC<br/>GORM 默认排序"]
|
||||
SortQ -->|"是"| Whitelist{"字段在白名单中?"}
|
||||
|
||||
Whitelist -->|"是"| CustomSort["db.Order(field + direction)"]
|
||||
Whitelist -->|"否"| DefaultSort
|
||||
|
||||
DefaultSort --> PagesQ{"要跳页还是滚动加载?"}
|
||||
CustomSort --> PagesQ
|
||||
|
||||
PagesQ --> |跳页| OffsetPaginate["LIMIT/OFFSET 分页<br/>简单直白"]
|
||||
PagesQ --> |滚动加载| CursorPaginate["游标分页<br/>性能好、防漏数据"]
|
||||
|
||||
OffsetPaginate --> DeepPage{是否深翻页?}
|
||||
DeepPage --> |浅页 ≤ 100| OK["直接用 ✓"]
|
||||
DeepPage --> |深页 > 100| CursorRecommend["推荐改用游标分页"]
|
||||
|
||||
|
||||
PagesQ -->|"跳页"| OffsetPaginate["LIMIT / OFFSET 分页<br/>简单直白,适合浅翻页"]
|
||||
PagesQ -->|"滚动加载"| CursorPaginate["游标分页<br/>性能好、防漏数据"]
|
||||
|
||||
OffsetPaginate --> DeepPage{"是否深翻页?"}
|
||||
DeepPage -->|"浅页 ≤ 100"| OK["直接使用 ✓"]
|
||||
DeepPage -->|"深页 > 100"| CursorRecommend["推荐改用游标分页"]
|
||||
|
||||
style Start fill:#4FC08D,color:#fff
|
||||
style CursorPaginate fill:#3B82F6,color:#fff
|
||||
style OK fill:#A0AEC0,color:#fff
|
||||
|
||||
Reference in New Issue
Block a user