因为现成orm难以满足动态条件拼接、多租户隔离和审计字段注入等需求,自研查询构建器通过可组合的query结构、sql与参数分离、白名单字段校验及空条件防护等设计实现可控与安全。

为什么不用现成的 ORM,而要自己写查询构建器
因为项目里有大量动态条件拼接、多租户 SQL 隔离、审计字段自动注入等需求,gorm 或 sqlx 的链式调用在复杂分支逻辑下容易失控,且难以统一拦截 SQL 生成过程。自定义构建器不是为了造轮子,而是把「条件怎么来」「字段怎么选」「参数怎么绑定」这些事收归到可控的结构里。
核心结构设计:从 Query 到 Where 的可组合性
不要一上来就写 SELECT * FROM 字符串拼接。真正可维护的构建器,必须让每个方法返回自身(支持链式),同时把 SQL 片段和参数分开存储——否则 sql.Query 执行时会因参数错位 panic。
-
Query结构体至少包含sql(字符串片段)、args([]interface{})、dialect(用于处理LIKE转义或占位符风格) -
Where方法不直接拼 SQL,而是往query.whereParts和query.args里追加;最终Build()再合并 - 避免在
Where里做 SQL 注入校验(如过滤字段名),应该在Column或Table方法里用白名单限制
示例:
q := NewQuery("users").Select("id", "name").Where("status = ?", 1).Where("created_at > ?", time.Now()) 这样调用后,q.Build() 返回的是安全的 "SELECT id, name FROM users WHERE status = ? AND created_at > ?" 和对应参数切片。
Scan 与 Rows 的解耦问题
很多自研构建器卡在结果映射这步:硬编码 rows.Scan(&u.ID, &u.Name) 无法复用,又不想引入反射开销。解决办法是让构建器只负责生成 SQL + 参数,把 Scan 交给调用方决定。
- 提供
Query.Exec(db)(返回sql.Result)和Query.Query(db)(返回*sql.Rows),不封装Scan - 如果需要结构体映射,建议配合
database/sql原生的sql.NullString等类型,或用reflect.StructTag做轻量字段匹配(仅限简单场景) - 别在构建器里实现
FindOne(&User{})这类方法——它会让接口职责模糊,也掩盖了 NULL 值处理的细节
边界情况:空条件、嵌套 OR、分页偏移
实际业务里最常崩的是这些地方:WHERE 后没条件却生成了 WHERE 关键字;OR 条件没加括号导致优先级错误;LIMIT ? OFFSET ? 的参数顺序和数据库方言不一致。
- 空
Where必须跳过整个WHERE子句,不能留空格或残留AND -
OrGroup方法应返回新子查询对象,内部用独立的whereParts,外层再用(...)包裹 -
Limit(n).Offset(m)要根据dialect选择LIMIT n OFFSET m(PostgreSQL/MySQL)还是TOP n START AT m+1(SQL Server)
比如 q.Where("a = ?", 1).OrGroup(func(q *Query) { q.Where("b = ?", 2).Where("c = ?", 3) }) 应生成 WHERE a = ? OR (b = ? AND c = ?),括号缺一不可。
真正难的不是写出第一个 SELECT,而是让 Where("id IN ?", ids) 在 ids 为空切片时不 panic,也不生成 IN () 这种语法错误——这种细节得在每个方法入口做 guard,而不是堆在 Build() 里补救。











