动态sql拼接不是gorm分页推荐路径,应使用where链式组合+order+limit/offset;raw sql易致参数错位、类型错误、sql注入、性能低下;动态条件须白名单校验order字段,游标分页需隔离查询实例并确保count复用同一query。

动态 SQL 拼接不是 GORM 分页的推荐路径,它绕过 GORM 的链式构建机制,极易引入 SQL 注入、类型不匹配和分页逻辑错乱;真要动态控制条件,应使用 Where 链式组合 + Order + Limit/Offset,而非手拼字符串。
为什么不能用 raw SQL 拼接做分页
手写 db.Raw("SELECT * FROM users WHERE status = ? AND created_at > ? ORDER BY id ASC LIMIT ? OFFSET ?") 看似灵活,但实际埋了三颗雷:
常见错误现象:参数顺序错位导致 OFFSET 被当 LIMIT 解析;created_at 传入字符串未转 time.Time,PostgreSQL 报 invalid input syntax for type timestamp;更隐蔽的是——你无法复用 GORM 的预处理机制,Scan(&users) 可能因字段名大小写/别名缺失而静默失败。
性能影响:GORM 的 Raw 查询不走 schema 缓存,每次执行都触发 SQL 解析+绑定,QPS 上不去。
安全风险:若某处把前端传的 sort_field 直接拼进 ORDER BY(如 "ORDER BY " + field),就等于开放 SQL 注入入口,连 id; DROP TABLE users 都能执行。
动态条件该用 Where 链式,不是字符串拼接
GORM 的 Where 支持条件累积,这才是动态过滤的正确打开方式:
使用场景:用户可选按 status、keyword、date_range 过滤,且任意组合
实操建议:
- 先初始化一个干净的
*gorm.DB实例:query := db.Model(&User{}) - 逐个追加条件:
if p.Status != "" { query = query.Where("status = ?", p.Status) } - 时间范围用
BETWEEN或双Where:query.Where("created_at BETWEEN ? AND ?", start, end) - 模糊搜索用
LIKE,注意手动加 %:query.Where("name LIKE ?", "%"+p.Keyword+"%") - 最后统一加
Order("id DESC")、Limit、Offset,避免在中间插入
Count(&total) 可直接复用同一 query 实例。
Order 字段动态化必须白名单校验
前端传 sort=id&order=desc 很常见,但 query.Order(p.Sort + " " + p.Order) 是高危操作:
容易踩的坑:
- 字段名未校验:传
sort=xxx就会生成ORDER BY xxx DESC,数据库报column "xxx" does not exist - 关键字未过滤:传
sort=id; DROP TABLE,虽 GORM 对Order不做参数绑定,但某些驱动可能尝试解析,行为不可控 - 复合排序失效:想支持
sort=created_at,id,但直接拼会导致ORDER BY created_at,id DESC—— 只有 id 生效 DESC,created_at 默认 ASC
- 定义排序白名单:
validSorts := map[string]bool{"id": true, "created_at": true, "name": true} - 校验字段:
if !validSorts[p.Sort] { p.Sort = "id" } - 显式控制方向:
dir := "DESC"; if p.Order == "asc" { dir = "ASC" } - 拼完整排序:
query.Order(p.Sort + " " + dir),不接受用户传整个 ORDER BY 子句
游标分页下动态条件更要隔离查询实例
当用游标分页(如 WHERE id > ? ORDER BY id ASC LIMIT ?)时,动态条件若没隔离好,游标值会被错误覆盖:
常见错误现象:首次请求带 status=active,返回 cursor=105;下一页请求漏传 status,后端却仍用带 status 的 query 构造游标条件,结果查不到数据或越界。
实操建议:
- 游标值(如
lastID)只用于构造 WHERE 条件,不参与其他过滤逻辑 - 所有动态条件(
status、keyword)必须在游标条件之前固定下来:query := db.Model(&User{}).Where("status = ?", p.Status) - 游标条件单独加:
if p.Cursor > 0 { query = query.Where("id > ?", p.Cursor) } - 确保
Count查询也复用同一query实例,否则总数和列表对不上
最易被忽略的一点:无论用 Where 链式还是游标,Order 的字段必须有索引,且和动态条件字段一起组成联合索引;否则 WHERE status = ? AND id > ? 这种查询仍会全表扫描。











