gorm无动态sql和原生分页,条件靠where/or/not累积拼接,分页依赖order+offset/limit或游标;where传nil自动跳过,or/not不重置条件;offset+limit必须显式order且字段有索引;游标分页需前端透传last_id,禁用混合条件。

GORM 没有“动态 SQL 构建”接口,也没有原生分页方法——所谓动态拼条件、自动分页,全是靠手动组合链式调用和显式控制实现的。别被封装库误导,底层逻辑就两条:条件靠 Where/Or/Not 拼,分页靠 Offset+Limit 或游标 Where 控制。
为什么 GORM 的 Where 链不是“动态 SQL”而是“条件累积”
GORM 的 Where 不是字符串模板,而是表达式构建器。每次调用都会追加 WHERE 子句条件,且支持 nil 安全:传 nil 或空值时自动跳过,不会生成 WHERE field = NULL 这种错误 SQL。
- 常见错误现象:
db.Where("name = ?", name).Where("status = ?", status).Find(&users)中,若name == "",仍会生成WHERE name = '' AND status = 1,查不到预期数据 - 正确写法是用 map 或 struct 过滤空值:
db.Where(map[string]interface{}{"status": status}).Where("name != ?", "").Find(&users) - 更稳妥的是用条件函数:
if name != "" { db = db.Where("name LIKE ?", "%"+name+"%") },显式控制是否拼入 - 注意
Or和Not不会重置前序条件,它们只是追加 OR/NOT 块,容易写出WHERE a=1 OR b=2 AND c=3这类优先级混乱的 SQL,建议用Scopes封装复合条件逻辑
Offset + Limit 分页必须加 Order,否则结果不可重现
没 Order 的分页等于随机抽样。数据库不保证无序查询的行顺序,同一条 Offset/Limit 查询执行两次,可能返回不同记录,甚至漏掉或重复某条。
- 必须显式调用
Order("id ASC")或Order("created_at DESC, id DESC");仅靠主键索引不能替代Order声明 - 排序字段必须有索引,否则
ORDER BY created_at DESC在百万表上会触发 filesort,拖慢整页响应 -
Offset必须在Limit前调用(虽 GORM v2 允许互换,但 MySQL 协议层要求先跳再取,固定顺序更稳) - 参数校验不能省:
page至少为 1,pageSize应限制在 1–100,避免Limit(10000)被恶意利用
游标分页绕不开 last_id 或 last_time,前端必须透传而非转换
游标分页不是“把 page 换成 cursor”,而是彻底放弃页码概念。它依赖上一页最后一条记录的排序字段值继续往后扫,所以前端必须原样传 last_id,后端不做任何计算或校验转换。
- 首次请求:
db.Order("id ASC").Limit(20).Find(&users),不带Where - 后续请求:
db.Where("id > ?", lastID).Order("id ASC").Limit(20).Find(&users),lastID必须来自users[len(users)-1].ID - 禁止混用:
Where("id > ? AND status = ?", lastID, 1)看似合理,但若status = 1的记录稀疏,会导致下一页数据量远少于 20 条,甚至为空 - 时间戳游标要防重复:
created_at DESC配合id DESC双排序,WHERE 条件写成WHERE (created_at, id) 才能覆盖毫秒级并发写入
Count 查询不准,八成是因为复用了同一个 *gorm.DB 实例
Count 不继承 Limit 和 Offset,但它会继承前面所有的 Where、Joins、Scopes。如果你在查列表前写了 db.Where(...).Joins(...),又直接拿这个 db 调 Count,那没问题;但若中间穿插了 Limit,或者想复用另一个查询实例,就极易出错。
- 最简隔离方式:
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total) - 关联查询总数难准:比如
Joins("JOIN profiles ON users.id = profiles.user_id")后Count,实际是按 JOIN 后的行数统计,而非 users 表去重数。此时应手写子查询或用SELECT COUNT(DISTINCT users.id) - 大数据量慎返
Total:可改查Limit(pageSize + 1),用是否有第pageSize + 1条来判断has_next,避免全表 COUNT
真正难的不是写对一次分页,而是在高并发写入、字段无唯一索引、前端参数不可信、关联模型嵌套这些现实约束下,让每一页都稳定返回且性能可控。游标分页的 last_id 从哪来、怎么传、谁负责校验,比 Offset 公式本身重要得多。











