分页查询

分页是 Web 开发的标配功能。数据量大时,不可能一次性加载全部,分页展示既节省资源又提升用户体验。

基本分页

Offset + Limit 实现:

page := 1
pageSize := 10

var users []User
db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&users)

获取总数

分页通常需要显示总条数和总页数:

var users []User
var total int64

db.Model(&User{}).Count(&total)

page := 1
pageSize := 10
db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&users)

totalPages := (total + int64(pageSize) - 1) / int64(pageSize)

分页结构体

封装分页结果:

type Page[T any] struct {
    List       []T   `json:"list"`
    Total      int64 `json:"total"`
    Page       int   `json:"page"`
    PageSize   int   `json:"page_size"`
    TotalPages int   `json:"total_pages"`
}

func NewPage[T any](list []T, total int64, page, pageSize int) *Page[T] {
    totalPages := int(total) / pageSize
    if int(total)%pageSize > 0 {
        totalPages++
    }
    return &Page[T]{
        List:       list,
        Total:      total,
        Page:       page,
        PageSize:   pageSize,
        TotalPages: totalPages,
    }
}

分页函数

通用分页函数:

func Paginate[T any](db *gorm.DB, page, pageSize int) (*Page[T], error) {
    if page < 1 {
        page = 1
    }
    if pageSize < 1 {
        pageSize = 10
    }
    if pageSize > 100 {
        pageSize = 100
    }

    var total int64
    if err := db.Count(&total).Error; err != nil {
        return nil, err
    }

    var list []T
    offset := (page - 1) * pageSize
    if err := db.Offset(offset).Limit(pageSize).Find(&list).Error; err != nil {
        return nil, err
    }

    return NewPage(list, total, page, pageSize), nil
}

var users []User
result, err := Paginate[User](db.Model(&User{}).Where("status = ?", "active"), 1, 10)

带条件的分页

func GetUserPage(db *gorm.DB, params QueryParams) (*Page[User], error) {
    query := db.Model(&User{})

    if params.Name != "" {
        query = query.Where("name LIKE ?", "%"+params.Name+"%")
    }
    if params.Status != "" {
        query = query.Where("status = ?", params.Status)
    }

    return Paginate[User](query.Order("id desc"), params.Page, params.PageSize)
}

游标分页

传统分页在大偏移量时性能差,游标分页是更好的选择:

type CursorPage[T any] struct {
    List     []T   `json:"list"`
    NextID   int64 `json:"next_id"`
    HasMore  bool  `json:"has_more"`
}

func CursorPaginate[T any](db *gorm.DB, lastID int64, pageSize int) (*CursorPage[T], error) {
    var list []T

    query := db.Limit(pageSize + 1)
    if lastID > 0 {
        query = query.Where("id > ?", lastID)
    }

    if err := query.Order("id asc").Find(&list).Error; err != nil {
        return nil, err
    }

    hasMore := len(list) > pageSize
    if hasMore {
        list = list[:pageSize]
    }

    var nextID int64
    if len(list) > 0 {
        nextID = reflect.ValueOf(list[len(list)-1]).FieldByName("ID").Int()
    }

    return &CursorPage[T]{
        List:    list,
        NextID:  nextID,
        HasMore: hasMore,
    }, nil
}

无限滚动

移动端常用无限滚动,本质是游标分页:

func LoadMore(db *gorm.DB, lastID int64, pageSize int) ([]User, bool, error) {
    var users []User

    query := db.Limit(pageSize + 1)
    if lastID > 0 {
        query = query.Where("id < ?", lastID)
    }

    if err := query.Order("id desc").Find(&users).Error; err != nil {
        return nil, false, err
    }

    hasMore := len(users) > pageSize
    if hasMore {
        users = users[:pageSize]
    }

    return users, hasMore, nil
}

分页优化

避免 Count 查询

Count 查询在大表上很慢。可以考虑:

  • 缓存总数
  • 只显示"加载更多",不显示总页数
  • 预估总数

延迟关联

大偏移量时,先查 ID 再关联:

var ids []int64
db.Model(&User{}).Offset(100000).Limit(10).Pluck("id", &ids)

var users []User
db.Where("id IN ?", ids).Find(&users)

使用覆盖索引

确保排序字段有索引,查询只走索引:

db.Select("id, name, age").Order("id").Offset(100000).Limit(10).Find(&users)

前端配合

分页参数通常从请求中获取:

func GetUsers(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))

    result, err := Paginate[User](db.Model(&User{}), page, pageSize)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }

    c.JSON(200, result)
}

小结

分页是基础但重要的功能。传统分页简单通用,游标分页性能更好。根据场景选择合适的方式,注意大偏移量问题。