huandu/go-sqlbuilder
GitHub: huandu/go-sqlbuilder
一个灵活强大的 Go 语言 SQL 语句构建库,提供全套 SQL 拼接工具和零配置轻量级 ORM,专注于高性能构建兼容标准库的参数化 SQL。
Stars: 1718 | Forks: 140
# Go 的 SQL builder
[](https://github.com/huandu/go-sqlbuilder/actions)
[](https://pkg.go.dev/github.com/huandu/go-sqlbuilder)
[](https://goreportcard.com/report/github.com/huandu/go-sqlbuilder)
[](https://coveralls.io/github/huandu/go-sqlbuilder?branch=master)
- [安装](#install)
- [使用说明](#usage)
- [基本用法](#basic-usage)
- [预定义的 SQL builder](#pre-defined-sql-builders)
- [构建 `WHERE` 子句](#build-where-clause)
- [在 builder 之间共享 `WHERE` 子句](#share-where-clause-among-builders)
- [构建 `ORDER BY` 子句](#build-order-by-clause)
- [为不同的系统构建 SQL](#build-sql-for-different-systems)
- [将 `Struct` 作为轻量级 ORM 使用](#using-struct-as-a-light-weight-orm)
- [嵌套 SQL](#nested-sql)
- [嵌套 `JOIN`](#nested-join)
- [在 builder 中使用 `sql.Named`](#use-sqlnamed-in-a-builder)
- [参数修饰符](#argument-modifiers)
- [自由格式 builder](#freestyle-builder)
- [克隆 builder](#clone-builders)
- [使用特殊语法构建 SQL](#using-special-syntax-to-build-sql)
- [在 `sql` 中插值 `args`](#interpolate-args-in-the-sql)
- [许可证](#license)
`sqlbuilder` 包提供了一整套 SQL 字符串拼接工具。它旨在帮助开发者构建兼容 Go 标准库 `sql.DB` 和 `sql.Stmt` 接口的 SQL 语句,并专注于优化 SQL 语句创建的性能和减少内存占用。
本包设计的初衷是打造一个独立于特定数据库驱动和业务逻辑的 SQL 构建库。它专门为了适应企业级环境中的各种需求而设计,包括使用自定义数据库驱动、遵循特定的操作规范、集成到异构系统中,以及在复杂场景下处理非标准 SQL。开源发布后,该包在大规模企业环境中经过了广泛测试,成功应对了每天数亿笔订单和近一千万笔交易的工作负载,从而彰显了其强大的性能和可扩展性。
本包不依赖于任何特定的数据库驱动,也不会自动建立与任何数据库系统的连接。它不假设生成的 SQL 会被直接执行,因此非常适用于各种需要构建类 SQL 语句的应用场景。同样,它也非常适合于在此基础上进行进一步开发,以创建更针对特定业务的数据库交互包、ORM 及其他类似工具。
## 安装
通过执行以下命令来安装此包:
```
go get github.com/huandu/go-sqlbuilder
```
## 使用说明
### 基本用法
我们可以使用此包快速构建 SQL 语句。
```
sql := sqlbuilder.Select("id", "name").From("demo.user").
Where("status = 1").Limit(10).
String()
fmt.Println(sql)
// Output:
// SELECT id, name FROM demo.user WHERE status = 1 LIMIT 10
```
在常见场景中,必须对所有用户输入进行转义。为此,需在一开始就初始化一个 builder。
```
sb := sqlbuilder.NewSelectBuilder()
sb.Select("id", "name", sb.As("COUNT(*)", "c"))
sb.From("user")
sb.Where(sb.In("status", 1, 2, 5))
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT id, name, COUNT(*) AS c FROM user WHERE status IN (?, ?, ?)
// [1 2 5]
```
### 预定义的 SQL builder
本包包含以下预定义的 builder。API 文档和使用示例可在 `godoc` 在线文档中查看。
- [Struct](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Struct):基于结构体定义创建 builder 的工厂。
- [CreateTableBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CreateTableBuilder):用于 `CREATE TABLE` 的 builder。
- [SelectBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#SelectBuilder):用于 `SELECT` 的 builder。
- [InsertBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#InsertBuilder):用于 `INSERT` 的 builder。
- [UpdateBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UpdateBuilder):用于 `UPDATE` 的 builder。
- [DeleteBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#DeleteBuilder):用于 `DELETE` 的 builder。
- [UnionBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UnionBuilder):用于 `UNION` 和 `UNION ALL` 的 builder。
- [CTEBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CTEBuilder):用于 Common Table Expression (CTE) 的 builder,例如 `WITH name (col1, col2) AS (SELECT ...)`。
- [Buildf](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Buildf):采用类似 `fmt.Sprintf` 语法的自由格式 builder。
- [Build](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Build):使用 [Args#Compile](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Args.Compile) 中定义的特殊语法的高级自由格式 builder。
- [BuildNamed](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#BuildNamed):使用 `${key}` 通过 map 中的键来引用值的高级自由格式 builder。
所有语句 builder 都实现了一个独特的方法 `SQL(sql string)`,允许在构建 SQL 期间将任意 SQL 片段插入到 builder 中。此功能对于编写包含 OLTP 或 OLAP 系统所需的非标准语法的 SQL 语句特别有用。
```
// Build a SQL to create a HIVE table.
sql := sqlbuilder.CreateTable("users").
SQL("PARTITION BY (year)").
SQL("AS").
SQL(
sqlbuilder.Select("columns[0] id", "columns[1] name", "columns[2] year").
From("`all-users.csv`").
String(),
).
String()
fmt.Println(sql)
// Output:
// CREATE TABLE users PARTITION BY (year) AS SELECT columns[0] id, columns[1] name, columns[2] year FROM `all-users.csv`
```
以下是为解决特殊情况而设计的几种实用方法。
- [Flatten](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Flatten) 支持将类似数组的变量递归转换为扁平化的 `[]interface{}` 切片。例如,调用 `Flatten([]interface{"foo", []int{2, 3}})` 将得到 `[]interface{}{"foo", 2, 3}`。此方法兼容 `In`、`NotIn`、`Values` 等 builder 方法,便于将特定类型的数组转换为 `[]interface{}` 或合并输入。
- [List](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#List) 的作用与 `Flatten` 类似,不同之处在于其返回值专门设计用作 builder 参数。例如,`Buildf("my_func(%v)", List([]int{1, 2, 3})).Build()` 会生成 SQL `my_func(?, ?, ?)` 及参数 `[]interface{}{1, 2, 3}`。
- [Raw](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Raw) 将字符串指定为参数中的“原始字符串”。例如,`Buildf("SELECT %v", Raw("NOW()")).Build()` 生成的结果为 SQL `SELECT NOW()`。
有关使用这些 builder 的详细说明,请参阅 [GoDoc 上提供的示例](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#pkg-examples)。
### 构建 `WHERE` 子句
`WHERE` 子句是 SQL 中最重要的部分。我们可以使用 `Where` 方法向 builder 添加一个或多个条件。
```
sb := sqlbuilder.Select("id").From("user")
sb.Where(
sb.In("status", 1, 2, 5),
sb.Or(
sb.Equal("name", "foo"),
sb.Like("email", "foo@%"),
),
)
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT id FROM user WHERE status IN (?, ?, ?) AND (name = ? OR email LIKE ?)
// [1 2 5 foo foo@%]
```
有许多用于构建条件的方法。
- [Cond.Equal](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Equal)/[Cond.E](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.E)/[Cond.EQ](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.EQ):`field = value`。
- [Cond.NotEqual](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotEqual)/[Cond.NE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NE)/[Cond.NEQ](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NEQ):`field <> value`。
- [Cond.GreaterThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GreaterThan)/[Cond.G](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.G)/[Cond.GT](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GT):`field > value`。
- [Cond.GreaterEqualThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GreaterEqualThan)/[Cond.GE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GE)/[Cond.GTE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GTE):`field >= value`。
- [Cond.LessThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LessThan)/[Cond.L](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.L)/[Cond.LT](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LT):`field < value`。
- [Cond.LessEqualThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LessEqualThan)/[Cond.LE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LE)/[Cond.LTE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LTE):`field <= value`。
- [Cond.In](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.In):`field IN (value1, value2, ...)`。
- [Cond.NotIn](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotIn):`field NOT IN (value1, value2, ...)`。
- [Cond.Like](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Like):`field LIKE value`。
- [Cond.ILike](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.ILike):`field ILIKE value`。
- [Cond.NotLike](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotLike):`field NOT LIKE value`。
- [Cond.NotILike](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotILike):`field NOT ILIKE value`。
- [Cond.Between](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Between):`field BETWEEN lower AND upper`。
- [Cond.NotBetween](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotBetween):`field NOT BETWEEN lower AND upper`。
- [Cond.IsNull](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.IsNull):`field IS NULL`。
- [Cond.IsNotNull](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.IsNotNull):`field IS NOT NULL`。
- [Cond.Exists](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Exists):`EXISTS (subquery)`。
- [Cond.NotExists](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotExists):`NOT EXISTS (subquery)`。
- [Cond.Not](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Not):`NOT expr`。
- [Cond.Any](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Any):`field op ANY (value1, value2, ...)`。
- [Cond.All](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.All):`field op ALL (value1, value2, ...)`。
- [Cond.Some](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Some):`field op SOME (value1, value2, ...)`。
- [Cond.IsDistinctFrom](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.IsDistinctFrom) `field IS DISTINCT FROM value`。
- [Cond.IsNotDistinctFrom](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.IsNotDistinctFrom) `field IS NOT DISTINCT FROM value`。
- [Cond.Var](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Var):任何值的占位符。
还有一些方法可用于组合条件。
- [Cond.And](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.And):使用 `AND` 运算符组合条件。
- [Cond.Or](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Or):使用 `OR` 运算符组合条件。
### 在 builder 之间共享 `WHERE` 子句
由于 `WHERE` 语句在 SQL 中非常重要,我们经常需要不断追加条件,甚至在不同的 builder 之间共享一些通用的 `WHERE` 条件。因此,我们将 `WHERE` 语句抽象为 `WhereClause` 结构体,可用于创建可复用的 `WHERE` 条件。
以下示例说明了如何将 `WHERE` 子句从 `SelectBuilder` 转移到 `UpdateBuilder`。
```
// Build a SQL to select a user from database.
sb := Select("name", "level").From("users")
sb.Where(
sb.Equal("id", 1234),
)
fmt.Println(sb)
ub := Update("users")
ub.Set(
ub.Add("level", 10),
)
// Set the WHERE clause of UPDATE to the WHERE clause of SELECT.
ub.WhereClause = sb.WhereClause
fmt.Println(ub)
// Output:
// SELECT name, level FROM users WHERE id = ?
// UPDATE users SET level = level + ? WHERE id = ?
```
### 构建 `UPDATE ... FROM`
`UpdateBuilder.From` 会为 PostgreSQL、SQLite 和 SQLServer 风格生成 `FROM` 子句(其他风格会忽略它)。当 CTE 包含使用 `CTETable` 创建的表时,这些表名将生成在任何显式 `From(...)` 表之前。
```
ub := PostgreSQL.NewUpdateBuilder()
ub.Update("users")
ub.Set(ub.Assign("name", "Huan Du"))
ub.From("people")
ub.Where("users.person_id = people.id")
sql, args := ub.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// UPDATE users SET name = $1 FROM people WHERE users.person_id = people.id
// [Huan Du]
```
请参阅 [WhereClause](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#WhereClause) 示例了解其用法。
### 构建 `ORDER BY` 子句
`ORDER BY` 子句通常用于对查询结果进行排序。本包提供了便捷的方法来构建具有正确排序方向的 `ORDER BY` 子句。
如果需要按具有不同排序方向(ASC/DESC)的多个列进行排序,请使用 `OrderByAsc` 和 `OrderByDesc` 方法。这些方法可以链式调用,以添加多个列及其特定的排序方式。
```
sb := sqlbuilder.NewSelectBuilder()
sb.Select("id", "name", "score").From("users")
sb.OrderByDesc("score").OrderByAsc("name")
sql, args := sb.Build()
fmt.Println(sql)
// Output:
// SELECT id, name, score FROM users ORDER BY score DESC, name ASC
```
旧的 `OrderBy` 方法结合 `Asc`/`Desc` 仍然可用,但已被弃用,因为它仅支持所有列使用单一的排序方向。新的 `OrderByAsc` 和 `OrderByDesc` 方法在处理多列时提供了更大的灵活性和清晰度。
### 为不同的系统构建 SQL
不同系统之间的 SQL 语法和参数占位符可能有所不同。为了解决这些差异,本包引入了一个称为“flavor”的概念。
目前,支持诸如 `MySQL`、`PostgreSQL`、`SQLite`、`SQLServer`、`CQL`、`ClickHouse`、`Presto`、`Oracle` 和 `Informix` 等 flavor。如果需要添加其他 flavor,请提交 issue 或 pull request。
默认情况下,所有 builder 都使用 `DefaultFlavor` 构建 SQL,默认设置为 `MySQL`。
为了提高可读性,可以使用 `PostgreSQL.NewSelectBuilder()` 来实例化一个带有 `PostgreSQL` flavor 的 `SelectBuilder`。所有 builder 都可以通过这种方式创建。
### 将 `Struct` 作为轻量级 ORM 使用
`Struct` 封装了类型信息和结构体字段,充当 builder 工厂。使用 `Struct` 的方法,可以生成预先配置好以供该结构体使用的 `SELECT`/`INSERT`/`UPDATE`/`DELETE` builder,从而节省时间并降低输入列名时出错的风险。
你可以定义一个结构体类型,并使用字段标签指导 `Struct` 生成相应的 builder。
```
type ATable struct {
Field1 string // If a field doesn't has a tag, use "Field1" as column name in SQL.
Field2 int `db:"field2"` // Use "db" in field tag to set column name used in SQL.
Field3 int64 `db:"field3" fieldtag:"foo,bar"` // Set fieldtag to a field. We can call `WithTag` to include fields with tag or `WithoutTag` to exclude fields with tag.
Field4 int64 `db:"field4" fieldtag:"foo"` // If we use `s.WithTag("foo").Select("t")`, columnes of SELECT are "t.field3" and "t.field4".
Field5 string `db:"field5" fieldas:"f5_alias"` // Use "fieldas" in field tag to set a column alias (AS) used in SELECT.
Ignored int32 `db:"-"` // If we set field name as "-", Struct will ignore it.
unexported int // Unexported field is not visible to Struct.
Quoted string `db:"quoted" fieldopt:"withquote"` // Add quote to the field using back quote or double quote. See `Flavor#Quote`.
Empty uint `db:"empty" fieldopt:"omitempty"` // Omit the field in UPDATE if it is a nil or zero value.
Expanded Meta `db:"expanded" fieldopt:"expand"` // Force a nested struct to expand even when sqlbuilder.NoExpand is true.
Payload Meta `db:"payload" fieldopt:"noexpand"` // Treat a nested struct as one column instead of expanding it.
// The `omitempty` can be written as a function.
// In this case, omit empty field `Tagged` when UPDATE for tag `tag1` and `tag3` but not `tag2`.
Tagged string `db:"tagged" fieldopt:"omitempty(tag1,tag3)" fieldtag:"tag1,tag2,tag3"`
// By default, the `SelectFrom("t")` will add the "t." to all names of fields matched tag.
// We can add dot to field name to disable this behavior.
FieldWithTableAlias string `db:"m.field"`
}
```
如果一个字段本身是一个结构体并且带有 `db` 标签,`Struct` 会将该标签视为数据库别名,并默认展开嵌套字段。这对于通过可复用的结构体构建 `JOIN` 投影非常有用。
```
type Post struct {
ID string `db:"id"`
Text string `db:"text"`
}
type Comment struct {
Body string `db:"body"`
}
type PostCommentJoined struct {
Post Post `db:"post"`
Comment Comment `db:"comment"`
}
joined := sqlbuilder.NewStruct(new(PostCommentJoined))
sql, _ := joined.SelectFrom("posts post").
Join("comments comment", "post.id = comment.post_id").
Build()
fmt.Println(sql)
// Output:
// SELECT post.id, post.text, comment.body FROM posts post JOIN comments comment ON post.id = comment.post_id
```
在首次使用 `Struct` 之前将 `sqlbuilder.NoExpand` 设置为 `true`,以将带有标签的嵌套结构体默认保持为单个列。启用此标志后,可以添加 `fieldopt:"expand"` 以让特定字段重新支持展开。
```
type Payload struct {
Key string `json:"key"`
}
type Row struct {
Payload Payload `db:"payload"`
Post Post `db:"post" fieldopt:"expand"`
}
sqlbuilder.NoExpand = true
st := sqlbuilder.NewStruct(new(Row))
fmt.Println(st.Columns())
// Output:
// [payload post.id post.text]
```
如果嵌套结构体应当保持为单个列,可以添加 `fieldopt:"noexpand"` 以退出自动展开。这对于由数据库驱动扫描进 Go 结构体的 JSON 列非常有用。
```
type Payload struct {
Key string `json:"key"`
}
type Row struct {
ID string `db:"id"`
Payload Payload `db:"payload" fieldopt:"noexpand"`
}
st := sqlbuilder.NewStruct(new(Row)).For(sqlbuilder.PostgreSQL)
sql, _ := st.SelectFrom("events e").Build()
fmt.Println(sql)
// Output:
// SELECT e.id, e.payload FROM events e
```
有关使用 `Struct` 的详细说明,请参阅[示例](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Struct)。
此外,`Struct` 还可以用作零配置的 ORM。与大多数需要预先配置数据库连接的 ORM 实现不同,`Struct` 无需任何配置即可运行,并且与任何兼容 `database/sql` 的 SQL 驱无缝协作。`Struct` 不会调用任何 `database/sql` API;它仅生成带有参数的适当 SQL 语句以供 `DB#Query`/`DB#Exec` 使用,或生成结构体字段地址数组供 `Rows#Scan`/`Row#Scan` 使用。
以下示例演示了如何将 `Struct` 用作 ORM。对于熟悉 `database/sql` API 的开发者来说,这应该相对简单。
```
type User struct {
ID int64 `db:"id" fieldtag:"pk"`
Name string `db:"name"`
Status int `db:"status"`
}
// A global variable for creating SQL builders.
// All methods of userStruct are thread-safe.
var userStruct = NewStruct(new(User))
func ExampleStruct() {
// Prepare SELECT query.
// SELECT user.id, user.name, user.status FROM user WHERE id = 1234
sb := userStruct.SelectFrom("user")
sb.Where(sb.Equal("id", 1234))
// Execute the query and scan the results into the user struct.
sql, args := sb.Build()
rows, _ := db.Query(sql, args...)
defer rows.Close()
// Scan row data and set value to user.
// Assuming the following data is retrieved:
//
// | id | name | status |
// |------|--------|--------|
// | 1234 | huandu | 1 |
var user User
rows.Scan(userStruct.Addr(&user)...)
fmt.Println(sql)
fmt.Println(args)
fmt.Printf("%#v", user)
// Output:
// SELECT user.id, user.name, user.status FROM user WHERE id = ?
// [1234]
// sqlbuilder.User{ID:1234, Name:"huandu", Status:1}
}
```
在许多生产环境中,表列名遵循 snake_case 命名约定,例如 `user_id`。相反,Go 中的结构体字段通常采用 CamelCase 以保持公共可访问性并满足 `golint` 要求。为每个结构体字段都使用 `db` 标签可能会显得冗余。为了简化操作,可以使用字段映射器函数来建立将结构体字段名映射到数据库列名的一致规则。
以下是关于字段映射器的一些重要注意事项:
- 字段标签的优先级高于字段映射器函数——因此,如果设置了 `db` 标签,则映射器将被忽略;
- 字段映射器仅在首次使用 Struct 创建 builder 时调用一次。
请参阅[字段映射器函数示例](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#FieldMapperFunc)以获取说明性示例。
### 嵌套 SQL
创建嵌套 SQL 非常简单:只需将 builder 用作参数进行嵌套即可。
这是一个说明性示例。
```
sb := sqlbuilder.NewSelectBuilder()
fromSb := sqlbuilder.NewSelectBuilder()
statusSb := sqlbuilder.NewSelectBuilder()
sb.Select("id")
sb.From(sb.BuilderAs(fromSb, "user")))
sb.Where(sb.In("status", statusSb))
fromSb.Select("id").From("user").Where(fromSb.GreaterThan("level", 4))
statusSb.Select("status").From("config").Where(statusSb.Equal("state", 1))
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT id FROM (SELECT id FROM user WHERE level > ?) AS user WHERE status IN (SELECT status FROM config WHERE state = ?)
// [4 1]
```
### 嵌套 `JOIN`
除了嵌套子查询之外,你还可以使用 `BuilderAs` 创建嵌套的 JOIN。当你需要与经过过滤或转换的数据集进行连接时,这特别有用。
以下示例展示了如何将表与嵌套子查询进行连接:
```
sb := sqlbuilder.NewSelectBuilder()
nestedSb := sqlbuilder.NewSelectBuilder()
// Build the nested subquery
nestedSb.Select("b.id", "b.user_id")
nestedSb.From("users2 AS b")
nestedSb.Where(nestedSb.GreaterThan("b.age", 20))
// Build the main query with nested join
sb.Select("a.id", "a.user_id")
sb.From("users AS a")
sb.Join(
sb.BuilderAs(nestedSb, "b"),
"a.user_id = b.user_id",
)
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT a.id, a.user_id FROM users AS a JOIN (SELECT b.id, b.user_id FROM users2 AS b WHERE b.age > ?) AS b ON a.user_id = b.user_id
// [20]
```
### 在 builder 中使用 `sql.Named`
`database/sql` 包中定义的 `sql.Named` 函数有助于在 SQL 语句中创建命名参数。对于需要在单个 SQL 语句中多次重用某个参数的场景,此功能至关重要。将命名参数合并到 builder 中非常简单:只需将它们视为常规参数即可。
这是一个示例。
```
now := time.Now().Unix()
start := sql.Named("start", now-86400)
end := sql.Named("end", now+86400)
sb := sqlbuilder.NewSelectBuilder()
sb.Select("name")
sb.From("user")
sb.Where(
sb.Between("created_at", start, end),
sb.GE("modified_at", start),
)
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT name FROM user WHERE created_at BETWEEN @start AND @end AND modified_at >= @start
// [{{} start 1514458225} {{} end 1514544625}]
```
### 参数修饰符
有几种可用的参数修饰符:
- `List(arg)` 封装了一系列参数。假设 `arg` 是一个切片或数组,例如一个包含三个整数的切片,它会被编译为 `?, ?, ?`,并在最终的参数中呈现为三个单独的整数。这是一种便捷工具,可在 `IN` 表达式或 `INSERT INTO` 语句的 `VALUES` 子句中使用。
- `TupleNames(names)` 和 `Tuple(values)` 有助于在 SQL 中表示元组语法。有关使用示例,请参阅 [Tuple](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#example-Tuple)。
- `Named(name, arg)` 指定一个命名参数。其功能仅限于 `Build` 或 `BuildNamed`,在此处它使用语法 `${name}` 定义一个命名占位符。
- `Raw(expr)` 将 `expr` 指定为 SQL 中的纯字符串,而不是参数。在构建 builder 期间,原始表达式会直接嵌入到 SQL 字符串中,从而无需使用 `?` 占位符。
### 自由格式 builder
builder 本质上是记录参数的一种手段。对于构建包含大量特殊语法元素(例如,用于数据库代理的特殊注释)的冗长 SQL 语句,可以使用 `Buildf`,它采用类似 `fmt.Sprintf` 的语法来格式化 SQL 字符串。
```
sb := sqlbuilder.NewSelectBuilder()
sb.Select("id").From("user")
explain := sqlbuilder.Buildf("EXPLAIN %v LEFT JOIN SELECT * FROM banned WHERE state IN (%v, %v)", sb, 1, 2)
sql, args := explain.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// EXPLAIN SELECT id FROM user LEFT JOIN SELECT * FROM banned WHERE state IN (?, ?)
// [1 2]
```
### 克隆 builder
`Clone` 方法使任何 builder 都能作为模板复用。你可以创建一次部分初始化的 builder(甚至可以作为全局变量),然后调用 `Clone()` 获取一个独立的副本,以便针对每个请求进行自定义。这避免了重复设置,同时保持共享模板不可变且能安全地用于并发操作。
支持 `Clone` 的 builder:
- [CreateTableBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CreateTableBuilder)
- [CTEBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CTEBuilder)
- [CTEQueryBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CTEQueryBuilder)
- [DeleteBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#DeleteBuilder)
- [InsertBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#InsertBuilder)
- [SelectBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#SelectBuilder)
- [UnionBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UnionBuilder)
- [UpdateBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UpdateBuilder)
示例:定义一个全局的 SELECT 模板并在每次调用时克隆它
```
package yourpkg
import "github.com/huandu/go-sqlbuilder"
// Global template — safe to reuse by cloning.
var baseUserSelect = sqlbuilder.NewSelectBuilder().
Select("id", "name", "email").
From("users").
Where("deleted_at IS NULL")
func ListActiveUsers(limit, offset int) (string, []interface{}) {
sb := baseUserSelect.Clone() // independent copy
sb.OrderByAsc("id")
sb.Limit(limit).Offset(offset)
return sb.Build()
}
func GetActiveUserByID(id int64) (string, []interface{}) {
sb := baseUserSelect.Clone() // start from the same template
sb.Where(sb.Equal("id", id))
sb.Limit(1)
return sb.Build()
}
```
相同的模板模式也适用于其他 builder。例如,保留一个包含表和通用 `SET` 子句的基础 `UpdateBuilder`,或者一个定义了可复用 CTE 的基础 `CTEBuilder`,然后根据需要执行 `Clone()` 并添加特定于查询的 `WHERE`/`ORDER BY`/`LIMIT`/`RETURNING`。
### 使用特殊语法构建 SQL
`sqlbuilder` 包内部引入了特殊语法来表示未编译的 SQL。若要利用此语法开发自定义工具,可以使用 `Build` 函数并传入必要的参数对其进行编译。
格式化字符串使用特殊语法来表示参数:
- `$?` 引用函数调用中按顺序提供的参数,其作用类似于 `fmt.Sprintf` 中的 `%v`。
- `$0`, `$1`, ..., `$n` 引用调用中提供的第 n 个参数;随后的 `$?` 将依次引用第 n+1 个及之后的参数。
- `${name}` 引用由 `Named` 使用指定 `name` 定义的命名参数。
- `$$` 表示字面意义上的 `"$"` 字符。
```
sb := sqlbuilder.NewSelectBuilder()
sb.Select("id").From("user").Where(sb.In("status", 1, 2))
b := sqlbuilder.Build("EXPLAIN $? LEFT JOIN SELECT * FROM $? WHERE created_at > $? AND state IN (${states}) AND modified_at BETWEEN $2 AND $?",
sb, sqlbuilder.Raw("banned"), 1514458225, 1514544625, sqlbuilder.Named("states", sqlbuilder.List([]int{3, 4, 5})))
sql, args := b.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// EXPLAIN SELECT id FROM user WHERE status IN (?, ?) LEFT JOIN SELECT * FROM banned WHERE created_at > ? AND state IN (?, ?, ?) AND modified_at BETWEEN ? AND ?
// [1 2 1514458225 3 4 5 1514458225 1514544625]
```
对于只需要使用 `${name}` 语法来引用命名参数的场景,请使用 `BuildNamed`。此函数会禁用除 `${name}` 和 `$$` 之外的所有特殊语法。
### 在 `sql` 中插值 `args`
某些类 SQL 驱动(例如用于 Redis 或 Elasticsearch 的驱动)未实现 `StmtExecContext#ExecContext` 方法。当 `len(args) > 0` 时,这些驱动程序会遇到问题。唯一的解决方法是将 `args` 直接插值到 `sql` 字符串中,然后使用该驱动程序执行生成的查询。
本包中的插值功能旨在提供“基本足够”的功能水平,而不是一种能与各种 SQL 驱动程序和 DBMS 系统的全面功能相媲美的能力。
_安全警告_:尽管我们在插值方法中努力对特殊字符进行转义,但此方法仍不如使用 SQL 驱动程序实现的 `Stmt` 安全。
此功能的灵感来源于 `github.com/go-sql-driver/mysql` 包中的插值功能。
这是一个专门针对 MySQL 的示例:
```
sb := MySQL.NewSelectBuilder()
sb.Select("name").From("user").Where(
sb.NE("id", 1234),
sb.E("name", "Charmy Liu"),
sb.Like("desc", "%mother's day%"),
)
sql, args := sb.Build()
query, err := MySQL.Interpolate(sql, args)
fmt.Println(query)
fmt.Println(err)
// Output:
// SELECT name FROM user WHERE id <> 1234 AND name = 'Charmy Liu' AND desc LIKE '%mother\'s day%'
//
```
这是针对 PostgreSQL 的示例,请注意它支持美元符号引用(dollar quoting):
```
// Only the last `$1` is interpolated.
// Others are not interpolated as they are inside dollar quote (the `$$`).
query, err := PostgreSQL.Interpolate(`
CREATE FUNCTION dup(in int, out f1 int, out f2 text) AS $$
SELECT $1, CAST($1 AS text) || ' is text'
$$
LANGUAGE SQL;
SELECT * FROM dup($1);`, []interface{}{42})
fmt.Println(query)
fmt.Println(err)
// Output:
//
// CREATE FUNCTION dup(in int, out f1 int, out f2 text) AS $$
// SELECT $1, CAST($1 AS text) || ' is text'
// $$
// LANGUAGE SQL;
//
// SELECT * FROM dup(42);
//
```
## 许可证
本包基于 MIT 许可证授权。有关更多信息,请参阅 LICENSE 文件。
标签:EVTX分析, 多线程, 日志审计