SQL Builder DSL 使用文档
本文档介绍 Airway 当前内置的 SQL Builder 体系,重点说明如何在项目中构建 SQL 查询,而不依赖重量级 ORM。
当前实现已经从“一个 Builder 尝试兼容所有数据库”的方向,调整为“一个核心 DSL + 三个方言 Builder”的结构:
lib/sql:底层通用 DSL 与内部实现lib/sql/pg:PostgreSQL Builderlib/sql/mysql:MySQL Builderlib/sql/sqlite:SQLite Builder
这套 Builder 体系的目标是:
- 保持 SQL 语义清晰,可预测地生成 SQL
- 使用命名参数,避免手写占位符和参数顺序错误
- 让业务代码在编译期就明确绑定数据库方言
- 把数据库能力边界直接体现在 API 暴露面上
- 让 PostgreSQL、SQLite 3、MySQL 8 各自按真实能力使用,而不是在运行时碰撞出错
- 通过表/字段引用减少裸字符串 SQL 片段
- 在简单场景保持足够轻量,在复杂场景提供更强约束
0. 先说结论
现在推荐的使用原则非常明确:
- 业务代码不要再默认直接依赖
lib/sql - 业务代码应该根据项目选定的数据库,明确导入一个方言包:
pg、mysql或sqlite lib/sql现在主要承担底层实现、共用类型和共用 helper 的职责lib/repo已经改为接收sql.Stmt接口,因此三套 Builder 都可以直接传给repo.Find、repo.Insert、repo.Update、repo.Delete
这样做的直接收益是:
- 如果某个数据库不支持某个能力,那么对应 Builder 就不暴露这个方法
- 代码会在编译期失败,而不是运行时才发现 SQL 不可执行
- API 的暴露面本身就成为数据库能力文档
例如:
pg.Builder支持DistinctOn、JoinLateral、ForShare、UpdateFrom、Using、ILike、JSONB/ARRAY helpermysql.Builder不支持这些方法,因此不会误用sqlite.Builder支持Returning,但不支持LATERAL JOIN、FOR UPDATE、ILIKE
0.1 你应该导入哪个包
PostgreSQL 项目:
import pg "github.com/daqing/airway/lib/sql/pg"MySQL 项目:
import mysql "github.com/daqing/airway/lib/sql/mysql"SQLite 项目:
import sqlite "github.com/daqing/airway/lib/sql/sqlite"查询条件 helper(Eq、Like、AllOf、FieldEq、MatchFields 等)在三个方言包里都做了重新导出,直接从方言包调用即可,例如 pg.Eq(...),无需单独导入条件包。
只有在以下情况,才应该直接依赖 lib/sql:
- 实现底层 SQL DSL 本身
- 编写方言 Builder 的包装层
- 编写
repo层这类需要接收统一接口的基础设施代码
0.2 当前结构
核心结构如下:
- lib/sql/base.go:定义
Stmt、H、基础类型 - lib/sql/builder.go:底层 Builder 实现
- lib/sql/pg/builder.go:PostgreSQL 方言包装
- lib/sql/mysql/builder.go:MySQL 方言包装
- lib/sql/sqlite/builder.go:SQLite 方言包装
- lib/repo/query.go:执行前的数据库方言转换和命名参数编译
0.3 能力矩阵
下表按“是否暴露为 Builder API”来理解,而不是数据库理论上是否完全支持某种 SQL 变体。
| 能力 | pg | mysql | sqlite |
|---|---|---|---|
| 基础 SELECT / INSERT / UPDATE / DELETE | Yes | Yes | Yes |
RETURNING | Yes | No | Yes |
DISTINCT ON | Yes | No | No |
JOIN LATERAL | Yes | No | No |
FULL JOIN | Yes | No | Yes |
FOR UPDATE | Yes | Yes | No |
FOR SHARE | Yes | No | No |
UPDATE ... FROM | Yes | No | No |
DELETE ... USING | Yes | No | No |
ON CONFLICT (columns) | Yes | Yes | Yes |
ON CONFLICT ON CONSTRAINT | Yes | No | No |
ILIKE / NOT ILIKE | Yes | No | No |
| JSONB helper | Yes | No | No |
| ARRAY helper | Yes | No | No |
INTERSECT ALL / EXCEPT ALL | Yes | No | Yes |
补充说明:
mysql.OnConflictDoNothing(...)最终会在repo层转换为INSERT IGNOREmysql.OnConflictDoUpdate(...)最终会在repo层转换为ON DUPLICATE KEY UPDATEsqlite.Returning(...)依赖 SQLite 3.35+sqlite.FullJoin(...)依赖 SQLite 3.39+
0.4 为什么不再追求一个统一 SQL 层
因为不同数据库之间的真实差异并不只是语法皮肤差异,而是能力边界差异:
- PostgreSQL 有
JSONB、ARRAY、DISTINCT ON、LATERAL JOIN - MySQL 没有
RETURNING,冲突更新语法也不是ON CONFLICT - SQLite 虽然支持
RETURNING,但不支持FOR UPDATE/FOR SHARE
如果继续把所有能力都堆进同一个对外 Builder,调用端会面临两个问题:
- IDE 自动补全会暴露大量当前数据库根本不能使用的方法
- 开发者只能靠文档或运行时报错记住哪些能力不能用
现在的结构是把“数据库能力差异”前移到 API 层,直接通过包和方法可见性表达出来。
0.5 推荐开发习惯
更合理的原则是:
- 单表、简单 CRUD、字段很少的查询:继续使用字符串 API 完全可以接受
- 多表 join、别名较多、schema-qualified table、复杂排序/返回列:优先使用表/字段引用写法
- PostgreSQL 独有能力:只在
pg包中书写和维护 - 如果项目未来可能切数据库,不要在通用查询中混入 PostgreSQL 专有 helper
当前 DSL 仍然建议采用两层风格:
- 默认写法:简单、直接
- 表/字段写法:在复杂 SQL 中提供更高可读性和安全性
为了降低样板代码,同时避免 Ref 和 Col 这类缩写过于抽象,现在更推荐使用更直白的命名:
TableOf("users")TableAlias("users", "u")users.Field("id")users.AllFields()
例如:
u := sql.TableAlias("users", "u")
b := sql.SelectFields(u.AllFields(), u.Field("email")).
FromTable(u).
Where(sql.FieldEq(u.Field("enabled"), true)).
OrderBy(u.Field("id").Desc())如果你希望 API 尽量大白话,推荐统一使用:
users.Field("id")users.AllFields()SelectFields(...)FieldEq(...)
这些命名在三个方言包里都做了重新导出,因此你可以这样写:
users := pg.TableAlias("users", "u")
b := pg.SelectFields(users.AllFields()).
FromTable(users).
Where(pg.FieldEq(users.Field("enabled"), true))兼容性说明:
Ref(...)、RefAs(...)仍然保留Col(...)、users.Col(...)仍然保留SelectRefs(...)、EqRef(...)、GroupByRefs(...)、ReturningRefs(...)这类旧名字也仍然保留- 但后续文档示例优先使用
TableOf(...)、TableAlias(...)、Field(...)、AllFields()、SelectFields(...)、FieldEq(...)
如果你在 Go 代码里查看类型签名,现在对外更推荐看到的是:
TableNameFieldName
它们分别是表引用和字段引用的对外类型名;原来的 TableRef、ColumnRef 继续保留兼容。
1. 核心概念
从实现上看,现在 Builder 分为 5 层:
- 方言层:
pg、mysql、sqlite - 语句层:
Select、Insert、Update、Delete - 条件层:
Where、AllOf、AnyOf、Compare、FieldEq等 - 表达式层:
Func、Array、Any、JSONGetText、Raw等 - 引用层:
TableOf、TableAlias、Field、TableFor、FieldFor
业务代码最常见的入口应该是这三个文件:
底层实现和共用类型仍然在这些文件中:
2. 基本用法
2.1 生成 SQL 和参数
所有 Builder 最终都通过 ToSQL() 导出 SQL 和命名参数:
b := pg.Select("*").From("users").Where(pg.Eq("enabled", true))
query, args := b.ToSQL()返回值:
query: 最终 SQL 字符串args:map[string]any,命名参数集合
如果你通过 repo 层执行,则通常不需要自己手工消费这两个返回值。
现在仓储层统一接收的是 sql.Stmt 接口,而不是 *sql.Builder。也就是说:
*pg.Builder可以直接传进去*mysql.Builder可以直接传进去*sqlite.Builder可以直接传进去- 原始
*sql.Builder也仍然可以传进去
仓储层中已经封装好了这一步,参考:
例如:
user, err := repo.FindOne[User](repo.CurrentDB(), pg.Select("*").From("users").Where(pg.Eq("id", 1)))2.2 基本 SELECT
b := sql.Select("*").
From("users").
Where(sql.Eq("enabled", true)).
OrderBy("id DESC").
Limit(20).
Offset(40)更推荐在复杂查询中使用表/字段写法:
users := sql.TableAlias("users", "u")
b := sql.SelectFields(users.AllFields()).
FromTable(users).
Where(sql.FieldEq(users.Field("enabled"), true)).
OrderBy(users.Field("id").Desc()).
Limit(20).
Offset(40)2.3 基本 INSERT
b := sql.Insert(sql.H{
"title": "hello",
"completed": false,
}).Into("todos")表写法:
todos := sql.TableOf("todos")
b := sql.Insert(sql.H{
"title": "hello",
"completed": false,
}).IntoTable(todos)2.4 基本 UPDATE
b := sql.Update("users").
Set(sql.H{"name": "new name"}).
Where(sql.Eq("id", 1))表/字段写法:
users := sql.TableOf("users")
b := sql.UpdateTable(users).
Set(sql.H{"name": "new name"}).
Where(sql.FieldEq(users.Field("id"), 1))2.5 基本 DELETE
b := sql.Delete().
From("users").
Where(sql.Eq("id", 1))表/字段写法:
users := sql.TableOf("users")
b := sql.DeleteFrom(users).
Where(sql.FieldEq(users.Field("id"), 1))说明:
- 从这一节开始,后面的示例为了简洁,仍然大量使用
sql.xxx作为抽象写法 - 在真实业务代码中,请把
sql替换为你项目选定的方言包,例如pg.xxx、mysql.xxx、sqlite.xxx - 如果示例里出现了 PostgreSQL 独有 helper,例如
ILike、Array、JSONGetText,那么它只应该出现在pg包代码里
3. 表/字段引用
这是复杂查询时更推荐的调用方式,但不是强制要求。
3.1 表引用
在 Go 类型层面,这类值通常会显示为 TableName。
users := sql.TableOf("users")
usersWithSchema := sql.TableOf("public", "users")
aliasedUsers := sql.TableAlias("users", "u")
schemaAliasedUsers := sql.TableOf("public", "users").As("u")表引用会在生成 SQL 时自动做 identifier quoting:
"public"."users" AS "u"3.2 字段引用
在 Go 类型层面,这类值通常会显示为 FieldName。
users := sql.TableAlias("users", "u")
idCol := users.Field("id")
emailCol := users.Field("email")
allCols := users.AllFields()
aliased := users.Field("email").As("user_email")辅助方法:
Asc()Desc()WithoutAlias():去掉 alias,只保留原引用
例如:
users.Field("created_at").Desc()3.3 Builder 中使用表/字段引用
users := sql.TableAlias("users", "u")
posts := sql.TableAlias("posts", "p")
b := sql.SelectFields(
users.Field("id"),
users.Field("email"),
posts.Field("title").As("post_title"),
).
FromTable(users).
LeftJoinTable(posts, sql.Compare(posts.Field("user_id"), "=", users.Field("id"))).
GroupByFields(users.Field("id"), users.Field("email"), posts.Field("title"))4. 条件 DSL
4.1 旧风格条件
适合兼容已有代码:
EqNotEqGtGteLtLteLikeNotLikeILike(仅pg)NotILike(仅pg)InNotInBetweenNotBetweenIsNullIsNotNull
示例:
sql.AllOf(
sql.Eq("status", "open"),
sql.Gte("priority", 10),
sql.Not(sql.IsNull("deleted_at")),
)4.2 表字段条件
这是现在更推荐的方式:
FieldEqFieldNotEqFieldGtFieldGteFieldLtFieldLteFieldLikeFieldILike(仅pg)Compare
示例:
users := sql.TableAlias("users", "u")
cond := sql.AllOf(
sql.FieldEq(users.Field("status"), "active"),
sql.FieldGte(users.Field("score"), 60),
sql.Compare(users.Field("role"), "=", sql.Any(sql.Array("admin", "editor"))),
)4.3 布尔组合
AllOf(...)AnyOf(...)Not(...)
示例:
cond := sql.AllOf(
sql.FieldEq(users.Field("enabled"), true),
sql.AnyOf(
sql.FieldEq(users.Field("role"), "admin"),
sql.FieldEq(users.Field("role"), "editor"),
),
)4.4 按表构建 map 条件
如果你的代码习惯传 map[string]any,可以直接使用:
MatchFields(table, vals)MatchTable(table, vals)
示例:
users := sql.TableAlias("users", "u")
cond := sql.MatchFields(users, sql.H{
"email": "dev@example.com",
"status": "active",
})这会把 map 中的 key 自动绑定到 users 表作用域下的列引用,而不是继续用裸字符串列名。
如果你的业务代码已经明确绑定某个数据库,推荐始终从方言包调用这些 helper,例如:
cond := pg.MatchFields(users, pg.H{
"email": "dev@example.com",
"status": "active",
})5. 表达式 DSL
常用表达式 helper:
Raw(sql)Expr(sql)ExprNamed(sql, args)Column(name)Func(name, args...)Op(left, operator, right)Cast(value, type)Excluded(column)Default()SubQuery(query)
其中:
Array(values...)、Any(value)、AllExpr(value)只在pg包中暴露Excluded(column)在三个方言包中都可见,但最常见于OnConflictDoUpdate(...)SubQuery(query)接收的是当前方言 Builder,而不是其他方言 Builder
示例:
sql.Compare(
sql.Func("COUNT", posts.Field("id")),
">",
0,
)sql.Compare(
users.Field("role"),
"=",
sql.Any(sql.Array("admin", "editor")),
)6. SELECT DSL
这一节列出的很多能力都来自底层 core builder,但不同方言包只会选择性暴露其中一部分。
6.1 选择列
Select("*")SelectColumns(...)SelectFields(...)Columns(...)Fields(...)
示例:
b := sql.SelectFields(users.Field("id"), users.Field("email")).
Columns(`COUNT("p"."id") AS post_count`)6.2 FROM / JOIN
From(tableName)FromTable(table)FromExpr(expr)FromSubQuery(query, alias)Join(...)JoinTable(...)LeftJoin(...)LeftJoinTable(...)RightJoin(...)RightJoinTable(...)FullJoin(...)FullJoinTable(...)CrossJoin(...)CrossJoinTable(...)JoinExpr(...)JoinLateral(...)LeftJoinLateral(...)
可用性:
FullJoin(...)/FullJoinTable(...):仅pg和sqliteJoinLateral(...)/LeftJoinLateral(...):仅pg
示例:
users := sql.TableAlias("users", "u")
posts := sql.TableAlias("posts", "p")
b := sql.SelectFields(users.Field("id"), posts.Field("title")).
FromTable(users).
LeftJoinTable(posts, sql.FieldEq(posts.Field("user_id"), users.Field("id")))6.3 DISTINCT / GROUP / HAVING / WINDOW
Distinct()DistinctOn(fields...)GroupBy(fields...)GroupByFields(columns...)Having(cond)Window(definitions...)
可用性:
DistinctOn(fields...):仅pgWindow(definitions...):pg、mysql、sqlite都提供
示例:
b := sql.SelectFields(users.Field("id")).
Columns(`COUNT("p"."id") AS post_count`).
FromTable(users).
LeftJoinTable(posts, sql.FieldEq(posts.Field("user_id"), users.Field("id"))).
GroupByFields(users.Field("id")).
Having(sql.Compare(sql.Func("COUNT", posts.Field("id")), ">", 0)).
Window("w AS (PARTITION BY id)")6.4 排序和分页
OrderBy(...)OrderBys(...)Limit(...)Offset(...)Page(page, perPage)
表/字段写法推荐:
b.OrderBy(users.Field("id").Desc())6.5 FOR UPDATE / FOR SHARE
For(clause)ForUpdate()ForShare()
可用性:
For(clause):仅pgForUpdate():pg、mysqlForShare():仅pg
6.6 CTE
With(name, query)WithRecursive(name, query)
示例:
activeUsers := sql.SelectFields(sql.Field("user_id")).
FromTable(sql.TableOf("sessions")).
Where(sql.FieldEq(sql.Field("revoked"), false))
b := sql.SelectFields(users.Field("id")).
With("active_users", activeUsers).
FromTable(users)6.7 集合查询
Union(query)UnionAll(query)Intersect(query)IntersectAll(query)Except(query)ExceptAll(query)
可用性:
Union、UnionAll、Intersect、Except:三个方言包都提供IntersectAll、ExceptAll:仅pg、sqlite
示例:
active := sql.SelectFields(sql.Field("id")).FromTable(sql.TableOf("users")).Where(sql.FieldEq(sql.Field("enabled"), true))
invited := sql.SelectFields(sql.Field("user_id").As("id")).FromTable(sql.TableOf("invitations")).Where(sql.FieldEq(sql.Field("accepted"), true))
b := active.UnionAll(invited).OrderBy(sql.Field("id").Desc())7. INSERT DSL
7.1 单行插入
b := sql.Insert(sql.H{
"title": "demo",
}).IntoTable(sql.TableOf("todos"))7.2 多行插入
b := sql.InsertRows(
sql.H{"id": 1, "name": "alpha"},
sql.H{"id": 2, "name": "beta"},
).Into("users")7.3 INSERT ... SELECT
InsertColumns(...)FromSelect(query, columns...)
示例:
source := sql.SelectFields(sql.Field("id"), sql.Field("email")).
FromTable(sql.TableOf("users")).
Where(sql.FieldEq(sql.Field("confirmed"), true))
b := sql.Insert(nil).
IntoTable(sql.TableOf("newsletter_subscribers")).
FromSelect(source, "user_id", "email")7.4 ON CONFLICT
OnConflictDoNothing(columns...)OnConflictOnConstraintDoNothing(constraint)OnConflictDoUpdate(columns, vals)OnConflictOnConstraintDoUpdate(constraint, vals)
可用性:
OnConflictDoNothing(columns...):三个方言包都提供OnConflictDoUpdate(columns, vals):三个方言包都提供OnConflictOnConstraintDoNothing(...):仅pgOnConflictOnConstraintDoUpdate(...):仅pg
MySQL 特别说明:
mysql.OnConflictDoNothing(...)在执行前会被转换成INSERT IGNOREmysql.OnConflictDoUpdate(...)在执行前会被转换成ON DUPLICATE KEY UPDATE- 这部分转换逻辑位于 lib/repo/query.go
示例:
b := sql.InsertRows(
sql.H{"id": 1, "name": "alpha"},
).Into("users").
OnConflictDoUpdate([]string{"id"}, sql.H{
"name": sql.Excluded("name"),
"updated_at": sql.Raw("NOW()"),
})7.5 RETURNING
Returning(fields...)ReturningFields(columns...)ReturningAll()
可用性:
pg:支持sqlite:支持mysql:不暴露
8. UPDATE DSL
Set(vals)UpdateFrom(tables...)Returning(...)ReturningFields(...)
可用性:
Set(vals):三个方言包都提供UpdateFrom(tables...):仅pgReturning(...)/ReturningFields(...):pg、sqlite
示例:
users := sql.TableOf("users")
b := sql.UpdateTable(users).
Set(sql.H{
"name": "new name",
"updated_at": sql.Raw("NOW()"),
}).
Where(sql.FieldEq(users.Field("id"), 1)).
ReturningFields(users.Field("id"), users.Field("name"))9. DELETE DSL
DeleteFrom(table)DeleteKey(column)DeleteKeyField(field)Using(tables...)Returning(...)ReturningFields(...)
可用性:
DeleteFrom(table)、DeleteKey(column)、DeleteKeyField(field):三个方言包都提供Using(tables...):仅pgReturning(...)/ReturningFields(...):pg、sqlite
当 DELETE 语句带有 ORDER BY / LIMIT / OFFSET 时,Builder 会自动退化为:
DELETE FROM table
WHERE id IN (
SELECT id FROM table
...
)示例:
events := sql.TableOf("audit", "events")
b := sql.DeleteFrom(events).
DeleteKeyField(sql.Field("audit", "events", "event_id")).
Where(sql.Compare(sql.Field("audit", "events", "kind"), "=", "login")).
OrderBy(sql.Field("audit", "events", "created_at").Desc()).
Limit(5).
ReturningFields(sql.Field("audit", "events", "event_id"))10. PostgreSQL JSONB / ARRAY helper
这一章是 PostgreSQL 专属能力,只应该在 pg 包中使用。
10.1 ARRAY
Array(...)Any(...)AllExpr(...)ArrayContains(left, right)对应@>ArrayContainedBy(left, right)对应<@ArrayOverlap(left, right)对应&&
示例:
sql.ArrayOverlap(users.Field("roles"), sql.Array("admin", "editor"))10.2 JSONB
JSONGet(left, key)对应->JSONGetText(left, key)对应->>JSONPath(left, path...)对应#>JSONPathText(left, path...)对应#>>JSONContains(left, right)对应@>JSONContainedBy(left, right)对应<@JSONHasKey(left, key)对应?JSONHasAnyKeys(left, keys...)对应?|JSONHasAllKeys(left, keys...)对应?&
示例:
cond := sql.AllOf(
sql.JSONHasKey(users.Field("profile"), "status"),
sql.Compare(sql.JSONGetText(users.Field("profile"), "status"), "=", "active"),
sql.JSONHasAnyKeys(users.Field("settings"), "beta", "dark_mode"),
)11. 迁移指引
如果你的代码之前统一使用:
import sql "github.com/daqing/airway/lib/sql"现在建议按下面方式迁移。
11.1 业务代码迁移
PostgreSQL 项目:
import pg "github.com/daqing/airway/lib/sql/pg"把:
sql.Select("*")
sql.Insert(...)
sql.Update(...)
sql.Delete()
sql.FieldEq(...)替换为:
pg.Select("*")
pg.Insert(...)
pg.Update(...)
pg.Delete()
pg.FieldEq(...)MySQL 和 SQLite 项目同理,只是前缀替换为 mysql 或 sqlite。
11.2 repo 层调用无需特殊改造
之前:
b := sql.Select("*").From("users")
user, err := repo.FindOne[User](repo.CurrentDB(), b)现在:
b := pg.Select("*").From("users")
user, err := repo.FindOne[User](repo.CurrentDB(), b)原因是 repo 现在接收的是 sql.Stmt 接口,而不是固定的 *sql.Builder。
11.3 什么时候还可以继续用 lib/sql
以下场景继续直接使用 lib/sql 是合理的:
- 你在维护底层 Builder 本身
- 你在编写一个新的方言包装层
- 你在写基础设施代码,希望接收
sql.Stmt而不关心具体数据库
普通业务查询不建议继续直接依赖 lib/sql,否则你会重新失去“按数据库能力裁剪 API”的收益。
11.4 常见迁移策略
建议这样逐步迁移:
- 先按项目数据库类型,把 import 从
lib/sql替换成对应方言包 - 确认项目还能通过编译
- 编译报错的位置,通常就是你正在调用该数据库不支持的能力
- 再针对这些位置决定:改写 SQL,还是保留为 PostgreSQL 专有查询
这也是这次改造最重要的目标之一:让“不兼容能力”在编译阶段暴露出来。
12. 与项目封装层配合使用
项目已经把部分 helper 和 CRUD 封装迁到了表/字段 DSL:
例如:
func FindById[T sql.Table](id sql.IdType) (*T, error) {
var t T
b := sql.FindByCond(t, sql.FieldEq(sql.FieldFor(t, "id"), id))
return repo.FindOne[T](repo.CurrentDB(), b)
}如果你在业务代码里继续封装自己的 repository/service,建议优先使用:
TableForFieldForFieldEqMatchTableSelectFieldsFromTable
如果你的封装层不想绑定具体数据库,也可以像 lib/repo 一样,直接接收 sql.Stmt 作为参数类型。
13. 推荐实践
13.1 优先使用表/字段引用写法
复杂查询推荐:
users := sql.TableAlias("users", "u")
sql.FieldEq(users.Field("id"), 1)简单查询继续这样写也完全可以:
sql.Eq("id", 1)只有在别名、多表、schema、复杂 SQL 表达式开始增多时,再切换到这种表/字段引用写法,收益才最明显。
13.2 原始 SQL 只留给复杂表达式
适合用原始字符串的场景:
- 聚合别名
- 窗口函数
- 特别复杂的数据库函数表达式
例如:
Columns(`ROW_NUMBER() OVER (ORDER BY "p"."created_at" DESC) AS rn`)13.3 单表 map 条件优先用 MatchTable
如果你是从 HTTP 参数或 service 层拿到一个 map[string]any,不要直接继续拼字段名,优先使用:
sql.MatchTable(t, vals)13.4 用测试验证生成 SQL
这套 DSL 的一个重要优势,就是生成 SQL 足够稳定,因此很适合直接测试 query string 和 args。
可以参考:
14. 当前限制
当前 DSL 已经支持大部分常见 PostgreSQL 查询,但仍然有一些能力还没有专门封装成更高层 API,例如:
- 聚合
FILTER (...) NULLS FIRST / NULLS LASTON CONFLICT ... WHERE- 更完整的窗口函数 DSL
- 更完整的 aggregate / alias builder
这些能力目前仍然可以通过 Raw(...)、Columns(...)、Expr(...)、Compare(...) 组合实现。
15. 参考示例
完整的调用示例可以直接看: