Cilium 仓库中的 Squirrel v1.5.4:Go 流式 SQL 生成器完整实战指南
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
Squirrel(github.com/Masterminds/squirrel)是一个为 Go 设计的流式(fluent)SQL 生成库,它帮助你从可组合的部件构建 SQL 查询、以编程方式处理条件逻辑,并直接对接database/sql执行。在 Cilium 仓库中,它以 v1.5.4 版本被 vendored 在 vendor/github.com/Masterminds/squirrel 下,作为 Helm(Helm v4 SQL 存储驱动)的依赖随项目分发。读完本文,你将掌握 Squirrel 的 Select/Insert/Update/Delete 构建器、条件谓词(Eq/Like/And/Or 等)、占位符格式转换、预编译语句缓存等能力,并能读懂其在真实存储驱动中的调用方式。
一、Squirrel 是什么:不是 ORM,而是 SQL 生成器
Squirrel 的定位非常明确——它不是一个 ORM(对象关系映射框架),而是一个 SQL 语句生成器。它不管理模型、不跟踪对象状态、不做自动建表,它的全部职责是:把 Go 中可组合的构建器(Builder)转换成合法的 SQL 字符串和参数列表。
import sq "github.com/Masterminds/squirrel"整个库的核心抽象是一个极其简单的接口,定义在 squirrel.go:
type Sqlizer interface { ToSql() (string, []interface{}, error) }ToSql返回三段内容:SQL 语句字符串、按顺序排列的参数切片(交给database/sql的Exec/Query使用)、以及错误。所有构建器(SelectBuilder、InsertBuilder、UpdateBuilder、DeleteBuilder)、所有条件表达式(Eq、Like、And、Or、Expr…)都实现这个接口,因此它们可以任意嵌套组合。
配合它工作的还有一组执行器接口(squirrel.go):
Execer:封装Exec(query, args...);Queryer:封装Query(query, args...);QueryRower:封装QueryRow(query, args...),返回RowScanner;BaseRunner=Execer+Queryer;Runner=Execer+Queryer+QueryRower。
如果你手头只有标准的*sql.DB,可以直接用WrapStdSql(stdSql)(squirrel.go)把它包装成 Squirrel 期望的Runner。
仓库事实:
vendor/modules.txt中记录该依赖为github.com/Masterminds/squirrel v1.5.4,要求go 1.14。
二、快速上手:从可组合部件构建查询
Squirrel 最核心的使用方式,是把一次查询拆成可独立组合的部件,然后层层叠加条件。
2.1 基础 Select 与条件叠加
import sq "github.com/Masterminds/squirrel" users := sq.Select("*").From("users").Join("emails USING (email_id)") active := users.Where(sq.Eq{"deleted_at": nil}) sql, args, err := active.ToSql() // sql == "SELECT * FROM users JOIN emails USING (email_id) WHERE deleted_at IS NULL" // args == []interface{}{}注意users与active是两个独立对象:Squirrel 的构建器是不可变的,Where返回的是追加了条件后的新构建器,原始users不受影响。这正是"从可组合部件构建"的含义——同一份基础查询可以派生出多个不同条件的查询。
2.2 多行 Insert
sql, args, err := sq. Insert("users").Columns("name", "age"). Values("moe", 13).Values("larry", sq.Expr("? + 5", 12)). ToSql() // sql == "INSERT INTO users (name,age) VALUES (?,?),(?,? + 5)" // args == []interface{}{"moe", 13, "larry", 12}Expr("? + 5", 12)允许你在 SQL 片段中嵌入带参数的表达式——这里把"年龄加 5"的计算留在数据库端完成,同时12仍作为参数传出。
2.3 直接执行查询
构建器不仅可以生成 SQL,还可以直接绑定数据库执行:
stooges := users.Where(sq.Eq{"username": []string{"moe", "larry", "curly", "shemp"}}) three_stooges := stooges.Limit(3) rows, err := three_stooges.RunWith(db).Query() // 行为等价于: rows, err := db.Query("SELECT * FROM users WHERE username IN (?,?,?,?) LIMIT 3", "moe", "larry", "curly", "shemp")这里有两个关键机制:
sq.Eq{"username": []string{...}}遇到切片值会自动展开为IN (?,?,?,?)并把每个元素作为独立参数;RunWith(db)把构建器与数据库绑定,之后可以直接调用Query()/Exec()/QueryRow()。
三、条件构建:让查询变得"像呼吸一样自然"
3.1 可选条件的叠加
Squirrel 在设计上让"按需追加条件"变得异常简单——因为构建器不可变,if分支里直接重新赋值即可:
if len(q) > 0 { users = users.Where("name LIKE ?", fmt.Sprint("%", q, "%")) }Where的签名是Where(pred interface{}, args ...interface{}),从源码 where.go 可以看到它接受多种谓词类型:
nil:空操作,什么都不加;- 实现了
Sqlizer的对象:调用其ToSql(); map[string]interface{}:自动转为Eq处理;string:直接作为 SQL 片段,args原样透传。
因此你可以混用字符串片段(如上面的"name LIKE ?")和结构化谓词(如sq.Eq{...})。
3.2 谓词家族(predicates)
expr.go中定义了一整套开箱即用的谓词(expr.go):
| 谓词 | 生成语义 | 示例 |
|---|---|---|
Eq{"col": v} | col = ?;值为nil时生成col IS NULL;值为切片时生成col IN (?,?,...);空切片生成(1=0) | Where(Eq{"id": 1})→id = ? |
NotEq{"col": v} | col <> ?;nil生成col IS NOT NULL;空切片生成(1=1) | Where(NotEq{"id": 1})→id <> ? |
Like{"col": v} | col LIKE ? | Where(Like{"name": "%irrel"}) |
NotLike | col NOT LIKE ? | |
ILike | col ILIKE ?(PostgreSQL 大小写不敏感) | Where(ILike{"name": "sq%"}) |
NotILike | col NOT ILIKE ? | |
Lt/LtOrEq | col < ?/col <= ? | Where(Lt{"id": 1})→id < ? |
Gt/GtOrEq | col > ?/col >= ? | Where(Gt{"id": 1})→id > ? |
And{...} | (A AND B AND ...) | 见下 |
Or{...} | (A OR B OR ...) | 见下 |
Expr(sql, args...) | 原样 SQL 片段 + 参数 | Expr("FROM_UNIXTIME(?)", t) |
几点实现细节值得注意(均有源码依据):
- 键排序确定性:
Eq等 map 型谓词在生成 SQL 前会通过getSortedKeys(expr.go)对键排序,保证同一 map 每次生成的 SQL 完全一致,便于缓存与测试。 - 空 map 语义:空的
Eq{}求值为(1=1)(恒真),配合后续 AND 不会改变结果。 AND连接符:Eq中多个键值对之间用AND连接,例如Eq{"col1": 1, "col2": 2}生成(col1 = ? AND col2 = ?)。And/Or连接词:空And求值为(1=1),空Or求值为(1=0)(expr.go),保证组合语义在边界情况下依然正确。
3.3 组合条件的嵌套
Or可以接受多个Sqlizer并整体加括号:
sq.Or{ sq.Eq{"col1": 1, "col2": 2}, sq.Eq{"col1": 3, "col2": 4}}生成:
WHERE (col1 = ? AND col2 = ?) OR (col1 = ? AND col2 = ?)这种写法在下一节的 FAQ 里还有更具体的应用。
四、PostgreSQL 支持:占位符格式(PlaceholderFormat)
Squirrel 默认使用问号占位符(?),而 PostgreSQL 需要$1, $2, ...这样的位置占位符。Squirrel 通过PlaceholderFormat机制解决跨数据库差异,支持四种内建格式(定义在 placeholder.go):
| 格式 | 占位符示例 | 适用场景 |
|---|---|---|
sq.Question | ? | MySQL / SQLite / 默认 |
sq.Dollar | $1, $2, $3 | PostgreSQL |
sq.Colon | :1, :2, :3 | Oracle 等 |
sq.AtP | @p1, @p2, @p3 | SQL Server |
PlaceholderFormat本身是一个接口(placeholder.go):
type PlaceholderFormat interface { ReplacePlaceholders(sql string) (string, error) }4.1 全局设置
通过StatementBuilder统一设置后,所有派生构建器都会继承:
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) // 你仍然用问号写占位符... sql, _, _ := psql.Select("*").From("elephants").Where("name IN (?,?)", "Dumbo", "Verna").ToSql() // ...squirrel 在 ToSql 阶段用 PlaceholderFormat 自动替换 // sql == "SELECT * FROM elephants WHERE name IN ($1,$2)"从 statement.go 可见,PlaceholderFormat只是给父构建器设置字段,后续通过StatementBuilder.Select(...)等创建的子构建器会继承这一配置。
4.2 单条查询内联设置 + 获取自增主键
你也可以在单个查询上直接设置占位符格式,并用Suffix("RETURNING \"id\"")追加 PostgreSQL 的RETURNING子句来取回自增 ID:
query := sq.Insert("nodes"). Columns("uuid", "type", "data"). Values(node.Uuid, node.Type, node.Data). Suffix("RETURNING \"id\""). RunWith(m.db). PlaceholderFormat(sq.Dollar) query.QueryRow().Scan(&node.id)4.3 转义问号:??→?
如果你需要写出字面意义上的?运算符(比如 PostgreSQL JSONB 的?/?|/?&操作符),插入两个问号??即可转义。底层实现位于 placeholder.go 的replacePositionalPlaceholders:遇到??时原样输出一个?并跳过,否则才递增位置序号。
例如原始 SQL:
SELECT * FROM nodes WHERE meta->'format' ??| array[?,?]使用Dollar格式生成:
SELECT * FROM nodes WHERE meta->'format' ?| array[$1,$2]这里的??|被还原为?|(PostgreSQL JSONB 存在性检查操作符),而后面的两个?则被替换为$1,$2。同理,DebugSqlizer(见后文)在 squirrel.go 中也处理了??转义。
五、StmtCache:自动预编译语句缓存
database/sql的*sql.Stmt预编译语句能显著提升重复查询的性能,但手动管理繁琐。Squirrel 提供了StmtCache来自动完成这件事:
// StmtCache 为你缓存 Prepared Stmts dbCache := sq.NewStmtCache(db) // StatementBuilder 让语法更整洁 mydb := sq.StatementBuilder.RunWith(dbCache) select_users := mydb.Select("*").From("users")StmtCache的实现(stmtcacher.go)非常直观:
- 内部维护
map[string]*sql.Stmt缓存,以 SQL 查询字符串为 key; Prepare(query)时先查缓存,命中直接返回,未命中则调用底层Preparer.Prepare并存入缓存;Exec、Query、QueryRow全部走Prepare路径,因此会自动复用预编译语句;- 内部用
sync.Mutex保证并发安全。
因为Eq等谓词按键排序、SQL 输出是确定性的,相同的逻辑查询总能生成完全一致的 SQL 字符串,这让 StmtCache 的缓存命中率最大化。
提示:
NewStmtCache的具体定义分文件存放(stmtcacher_ctx.go与stmtcacher_noctx.go),分别对应 Go ≥ 1.8 与更早版本的上下文支持差异;在 v1.5.4(要求 go 1.14)下使用的是stmtcacher_ctx.go。
六、仓库内真实用例:Helm SQL 存储驱动
在 Cilium 仓库的 vendored 依赖中,Helm v4 的 SQL 存储驱动 vendor/helm.sh/helm/v4/pkg/storage/driver/sql.go 是 Squirrel 在生产级代码中的典型示范。该驱动维护了一个statementBuilder sq.StatementBuilderType字段(sql.go),初始化时统一使用 PostgreSQL 方言:
statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),6.1 查询(Get / List / Query)
读取 release 记录时,用Select+From+ 多个Where(sq.Eq{...})叠加条件(sql.go):
qb := s.statementBuilder. Select(sqlReleaseTableBodyColumn). From(sqlReleaseTableName). Where(sq.Eq{sqlReleaseTableKeyColumn: key}). Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}) query, args, err := qb.ToSql() if err != nil { s.Logger().Debug("failed to build query", slog.Any("error", err)) return nil, err } // 之后用 sqlx 执行:s.db.Get(&record, query, args...)按标签过滤的Query方法则展示了"循环中动态追加条件"的经典模式(sql.go):
sb := s.statementBuilder. Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn). From(sqlReleaseTableName) // 逐个标签动态追加条件 for key, value := range labels { sb = sb.Where(sq.Eq{key: value}) } sb = sb.Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}) query, args, err := sb.ToSql() // ...s.db.Select(&records, query, args...)6.2 写入(Insert / Update / Delete)
写入路径使用statementBuilder.Insert(...).Columns(...).Values(...)构建多行插入(sql.go),更新则使用Update+Set+Where(sql.go):
query, args, err := s.statementBuilder. Update(sqlReleaseTableName). Set(sqlReleaseTableBodyColumn, body). Set(sqlReleaseTableNamespaceColumn, namespace). Where(sq.Eq{sqlReleaseTableKeyColumn: key}). Where(sq.Eq{sqlReleaseTableNamespaceColumn: namespace}). ToSql()删除操作则先Select出待删记录,再用Delete构建删除语句(sql.go)。
这个真实用例印证了 Squirrel 的几个关键设计优势:统一管理占位符方言、条件叠加的简洁性、ToSql()后把 SQL 与参数交给第三方库(如 sqlx)执行的灵活性。
七、FAQ:常见陷阱与答疑
7.1 复合键 / 元组 IN 查询怎么写?
Squirrel 不显式支持元组(tuple)语法,即无法直接写出WHERE (col1, col2) IN ((1,2),(3,4))。但可以用Or+And组合达到相同效果(且执行计划一致):
sq.Or{ sq.Eq{"col1": 1, "col2": 2}, sq.Eq{"col1": 3, "col2": 4}}WHERE (col1 = ? AND col2 = ?) OR (col1 = ? AND col2 = ?)7.2 为什么Eq{"mynumber": []uint8{1,2,3}}不生成 IN 查询?
这是因为[]uint8在 Go 中与[]byte是同一类型(byte是uint8的别名),而database/sql对[]byte有特殊处理(视为二进制数据)。Squirrel 无法区分二者,因此[]uint8{1,2,3}会被当作字节串值而非列表。如果需要 IN 查询,请改用[]int、[]string等类型。
7.3 有些特性文档不完整怎么办?
Squirrel 官方的态度是:测试即文档。README 明确建议把测试当作文档的一部分来阅读——仓库内的_test.go文件(如select_test.go、expr_test.go等)覆盖了大量复杂查询的写法,是学习高级用法的第一手资料。
八、进阶:源码级原理速览
8.1 构建器是不可变的数据结构
从 select.go 可以看到selectData只是一个保存字段的普通结构体:
type selectData struct { PlaceholderFormat PlaceholderFormat RunWith BaseRunner Prefixes []Sqlizer Options []string Columns []Sqlizer From Sqlizer Joins []Sqlizer WhereParts []Sqlizer GroupBys []string HavingParts []Sqlizer OrderByParts []Sqlizer Limit string Offset string Suffixes []Sqlizer }Squirrel 底层依赖github.com/lann/builder包实现不可变追加:每个链式方法都基于当前 builder 复制一份并追加字段,返回新实例。ToSql()则遍历这些字段按序拼装 SQL(select.go),并在最后统一调用PlaceholderFormat.ReplacePlaceholders完成占位符替换。
8.2 从ToSql()到执行的三层封装
顶层辅助函数(squirrel.go)统一了执行路径:
func ExecWith(db Execer, s Sqlizer) (res sql.Result, err error) { query, args, err := s.ToSql() if err != nil { return } return db.Exec(query, args...) }每个具体 builder(如selectData)的Query()/Exec()/QueryRow()(select.go)在调用前会检查RunWith是否已设置:
- 未设置则返回预定义错误
RunnerNotSet("cannot run; no Runner set (RunWith)"); QueryRow额外检查RunWith是否实现了QueryRower,否则返回RunnerNotQueryRunner。
8.3 调试利器:DebugSqlizer
DebugSqlizer(s Sqlizer) string(squirrel.go)会把参数内联进 SQL,输出近似可读的语句,方便打印日志排查。注意源码注释明确警告:该函数仅用于调试——输出结果不保证是合法 SQL,且把不可信用户输入内联进去执行是危险的。
九、使用建议与注意事项
- 保持 SQL 注入安全:永远通过 Squirrel 的参数机制(
?占位符 + args)传值,不要手工拼接用户输入进 SQL 字符串;字符串形式的Where片段同样应使用占位符。 - 利用确定性输出:
Eq等 map 谓词按键排序、And/Or输出稳定,这意味着同一逻辑生成的 SQL 字符串完全一致,可放心配合StmtCache或应用层 SQL 缓存。 - 方言切换:开发环境(SQLite)与生产环境(PostgreSQL)往往共用一套构建逻辑,仅在创建
StatementBuilder时指定不同的PlaceholderFormat即可切换。 - 性能敏感场景:
StmtCache适合高频重复查询;同时注意ToSql()每次都会完整拼装字符串,极端高频场景可考虑缓存ToSql()结果。 - 以测试为参考:复杂查询(嵌套子查询、
CASE表达式、Alias别名列、ConcatExpr拼接表达式等)的具体写法,以本仓库 vendor/github.com/Masterminds/squirrel 目录下的*_test.go测试文件为准。
十、License
Squirrel 以 MIT License 发布(见 vendor/github.com/Masterminds/squirrel/LICENSE),可以在商业与开源项目中自由使用、修改和再分发。
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考