gorm分页需解决总条数查询拖慢主查、count与主查逻辑一致、分页参数校验收口三问题;须隔离count查询、用指针字段兜底默认值、join时自动子查询计数,并确保statement不被污染。

直接用 GORM 自带的 Offset/Limit 做分页不难,但通用 CRUD 生成器里必须解决三个实际问题:总条数查询不能拖慢主查询、Count 和主查询逻辑必须一致、分页参数默认值和边界校验得收口到一处——否则每个接口都手动写一遍,迟早出错。
为什么不能直接在 Scopes 里调 Count
常见错误是把 Count 和主查询塞进同一个 Scopes 函数里,比如:
func Page(page *Page) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
var total int64
db.Count(&total) // ❌ 这里会执行带 WHERE/JOIN 的 COUNT,但没做 SELECT 子句剥离
return db.Offset(...).Limit(...)
}
}
问题在于:Count 会继承前面所有 Where、Joins、Select,而 Select("xxx") 会让 COUNT 失效(MySQL 报错或返回 0)。更糟的是,如果主查询用了 Joins,COUNT 可能因笛卡尔积虚高。
- 正确做法是复刻原始
Statement,清空Select、Order、Limit、Offset,只保留Where和Joins - 用
db.Session(&gorm.Session{NewDB: true})或显式新建*gorm.DB实例来隔离 COUNT 查询 - 避免在
Count前调Find或First,否则 Statement 被污染
Page 结构体字段必须用指针 + 显式默认值
Current 和 Size 字段如果定义成 int,前端传空或 0 就无法区分“未传”和“传了 0”。生成器必须强制用指针类型,并在 Scopes 函数里做兜底:
if page.Current == nil || *page.Current 100 {
size := 10
page.Size = &size
}
-
Size上限建议硬编码为 100,防 SQL 注入和 OOM(比如前端传size=999999) - 不要依赖中间件统一校验——CRUD 生成器产出的 handler 是独立的,校验逻辑必须内聚在分页模块里
-
Total字段必须是*int64,因为Count(&total)要求地址,且数据库总条数可能超 int32
JOIN 场景下 COUNT 必须用子查询
当主查询含 Joins("JOIN t_user ON ...") 时,直接 Count 会统计连接后的行数。例如 1 个订单关联 3 个商品,这条订单会被 COUNT 成 3 次。
正确解法是让生成器自动识别是否含 Joins,并改用子查询 COUNT:
subQuery := db.Session(&gorm.Session{NewDB: true}).Table("orders").
Select("COUNT(*)").Joins("JOIN t_user ON orders.user_id = t_user.id").
Where("orders.status = ?", "paid")
db.Raw("(?)", subQuery).Scan(&total)
- 不能靠人工判断——生成器要解析
Statement.Joins切片,非空就走子查询路径 - 子查询里必须复现全部
Where条件,否则总数不准;但Order、Limit、Offset必须剔除 - MySQL 8.0+ 支持
COUNT(DISTINCT orders.id),但跨库兼容性差,子查询更稳
最易被忽略的一点:分页模块和 CRUD 生成器的耦合点不在 SQL,而在 Statement 生命周期。每次调 Scopes(Page) 都要确保它不污染后续链式调用——比如 db.Scopes(Page(p)).Where(...).Find(),中间的 Where 必须生效,这要求分页函数内部新建 DB 实例,而不是复用传入的 db 指针。











