sqlx: 扩展标准sql库

sqlx是一个用于扩展标准库database/sql的库,它提供了一些额外的功能,使得在Go中使用sql更加方便。sqlx的目标是保持database/sql的简单性,同时提供更多的功能。

sqlx 为 Go 的标准 database/sql 库提供了一组扩展。sqlx 中的 sql.Connsql.DBsql.TXsql.Stmtsql.Rowssql.Row 等版本都保留了底层接口不变,因此它们的接口是标准接口的超集。这使得将使用 database/sql 的现有代码库与 sqlx 集成相对容易。

主要的额外概念有:

  • 将行映射到结构体(支持嵌入结构体)、Map和切片
  • 支持命名参数,包括预编译语句
  • 使用 GetSelect 快速从查询到结构体/切片

sqlx 的目的是无缝地封装 database/sql,并提供在开发数据库驱动的应用程序时有用的便捷方法。它不会改变任何底层的 database/sql 方法。相反,所有扩展行为都是通过在包装类型上定义的新方法来实现的。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type Conn struct {
*sql.Conn
driverName string
unsafe bool
Mapper *reflectx.Mapper
}
type DB struct {
*sql.DB
driverName string
unsafe bool
Mapper *reflectx.Mapper
}
type Stmt struct {
*sql.Stmt
unsafe bool
Mapper *reflectx.Mapper
}
type Tx struct {
*sql.Tx
driverName string
unsafe bool
Mapper *reflectx.Mapper
}
type Rows struct {
*sql.Rows // 嵌入
unsafe bool
Mapper *reflectx.Mapper
// these fields cache memory use for a rows during iteration w/ structScan
started bool
fields [][]int
values []interface{}
}

可以看到,它的核心类型都是对标准库的封装,然后在此基础上提供了更多的功能。

它是2013年发布,已经有11年的历史了,也许为了保持兼容,它没有对泛型提供支持,甚至interface{}也没有改为any,还支持Go 1.10的版本。

本文假定你已经有了Go开发数据库程序的基础。如果你还不了解,建议你阅读下面的材料:

本文是编译自作者写的sqlx图解指南

引入sqlx库以及sqlite3驱动:

1
2
$ go get github.com/jmoiron/sqlx
$ go get github.com/mattn/go-sqlite3

sqlx 的设计初衷是让用户感觉与 database/sql 一样。它主要有 5 种 handler 类型:

  • sqlx.Conn - 类似于 sql.Conn,表示一个数据库连接
  • sqlx.DB - 类似于 sql.DB,表示一个数据库连接池
  • sqlx.Tx - 类似于 sql.Tx,表示一个事务
  • sqlx.Stmt - 类似于 sql.Stmt,表示一个预处理语句
  • sqlx.NamedStmt - 表示一个支持命名参数的预处理语句

这些 handler 类型都嵌入了它们的 database/sql对应类型,这意味着当你调用 sqlx.DB.Query 时,你实际上调用的是与 sql.DB.Query 相同的代码。这使得它易于引入现有的代码库中。

除了这些,还有 2 种游标类型:

  • sqlx.Rows - 类似于 sql.Rows,是从 Queryx 返回的游标,多行结果
  • sqlx.Row - 类似于 sql.Row,是从 QueryRowx 返回的结果,单行结果

与 handler 类型一样,sqlx.Rows 嵌入了 sql.Rows。由于无法访问底层实现,sqlx.Row 是对 sql.Row 的部分重新实现,同时保留了标准接口。

连接数据库

一个 DB 实例并不是连接,而是一个表示数据库的抽象。这就是为什么创建 DB 时不会返回错误也不会引发恐慌(panic)。它在内部维护了一个连接池,并会在首次需要连接时尝试连接。你可以通过 Open 方法创建一个 sqlx.DB,或者通过 NewDb 方法从现有的 sql.DB 创建一个新的sqlx.DB handler :

1
2
3
4
5
6
7
8
9
10
var db *sqlx.DB
// 完全与内置的一样
db = sqlx.Open("sqlite3", ":memory:")
// 从现有的sql.DB创建一个新的sqlx.DB
db = sqlx.NewDb(sql.Open("sqlite3", ":memory:"), "sqlite3")
// 强制连接并测试是否成功
err = db.Ping()

在某些情况下,你可能希望同时打开数据库并建立连接,例如,在初始化阶段捕获配置问题。你可以使用 Connect 方法一次性完成这个操作,它会打开一个新的数据库并尝试进行 Ping 操作。MustConnect 变种在遇到错误时会触发 panic,适合在你的包的模块级别使用:

1
2
3
4
5
6
7
var err error
// 打开并连接数据库
db, err = sqlx.Connect("sqlite3", ":memory:")
// 打开并连接数据库,遇到错误时触发panic
db = sqlx.MustConnect("sqlite3", ":memory:")

基本查询

sqlx 中的 handler 类型实现了与 database/sql 相同的基本动词来查询你的数据库:

  • Exec(...) (sql.Result, error) - 与 database/sql 中的方法没有变化
  • Query(...) (*sql.Rows, error) - 与 database/sql 中的方法没有变化
  • QueryRow(...) *sql.Row - 与 database/sql 中的方法没有变化

以下是内置方法的扩展:

  • MustExec() sql.Result -- 执行 Exec,但遇到错误时会触发 panic
  • Queryx(...) (*sqlx.Rows, error) - 执行 Query,但返回一个 sqlx.Rows
  • QueryRowx(...) *sqlx.Row -- 执行 QueryRow,但返回一个 sqlx.Row

还有以下新的语义:

  • Get(dest interface{}, ...) error
  • Select(dest interface{}, ...) error

现在,我们从未改变的接口开始,一直介绍到新的语义,并解释它们的使用方法。

执行 Exec

ExecMustExec 从连接池中获取一个连接,并在服务器上执行提供的语句。对于不支持即席(ad-hoc)查询执行的驱动程序,可能会在幕后创建一个预处理语句来执行。在返回结果之前,连接会被返回到连接池中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
schema := `CREATE TABLE place (
country text,
city text NULL,
telcode integer);`
// 执行一个查询
result, err := db.Exec(schema)
// 或者,你可以使用MustExec,在错误时会触发panic
cityState := `INSERT INTO place (country, telcode) VALUES (?, ?)`
countryCity := `INSERT INTO place (country, city, telcode) VALUES (?, ?, ?)`
db.MustExec(cityState, "Hong Kong", 852)
db.MustExec(cityState, "Singapore", 65)
db.MustExec(countryCity, "South Africa", "Johannesburg", 27)

Result有两种可能的数据:LastInsertId()RowsAffected(),这些数据的可用性取决于驱动程序。例如,在 MySQL 中,如果插入的表有自增主键,则 LastInsertId() 将可用,但在 PostgreSQL 中,这些信息只能通过使用 RETURNING 子句从普通行游标中检索。

绑定变量 bindvars

内部称为绑定变量的 ? 查询占位符非常重要;您应该始终使用这些占位符向数据库发送值,因为它们可以防止 SQL 注入攻击。database/sql 不会对查询文本进行任何验证;它会原样发送到服务器,同时发送编码后的参数。除非驱动程序实现了特殊接口,否则查询会在执行之前先在服务器上准备。因此,绑定变量是特定于数据库的:

  • MySQL 使用上面展示的 ? 变体
  • PostgreSQL 使用枚举的 $1,$2等绑定变量语法
  • SQLite 接受 ?$1 语法
  • Oracle 使用 :name 语法
  • 其他数据库可能有所不同。您可以使用 sqlx.DB.Rebind(string) string函数和 ? 绑定变量语法来获取适合在当前数据库类型上执行的查询。
    关于绑定变量的一个常见误解是它们用于插值。它们仅用于参数化,并且不允许更改 SQL 语句的结构。例如,使用绑定变量来尝试参数化列名或表名将无法工作:
1
2
3
4
5
// 无法工作
db.Query("SELECT * FROM ?", "mytable")
// 也无法工作
db.Query("SELECT ?, ? FROM people", "name", "location")

查询 Query

Query 是使用 database/sql 执行查询并返回行结果的主要方法。Query 返回一个 sql.Rows 对象和一个错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 从数据库获取所有地点
rows, err := db.Query("SELECT country, city, telcode FROM place")
// 遍历每一行
for rows.Next() {
var country string
// 注意city可能为NULL,所以我们使用NullString类型
var city sql.NullString
var telcode int
err = rows.Scan(&country, &city, &telcode)
}
// 检查错误
err = rows.Err()

你应该把 Rows 当作数据库游标来处理,而不是一个具体化的结果列表。尽管驱动程序缓冲行为可能有所不同,但通过 Next() 进行迭代是限制大型结果集内存使用量的好方法,因为你一次只扫描一行。Scan() 使用反射将 SQL 列返回类型映射到 Go 类型,如 string[]byte 等。如果你没有遍历完整个结果集,请确保调用 rows.Close() 将连接返回给连接池!

Query 返回的错误是可能在服务器准备或执行期间发生的任何错误。这可能包括从连接池中获取了有问题的连接,尽管 database/sql 会重试 10 次以尝试找到或创建一个工作连接。一般来说,错误会由于错误的 SQL 语法、类型不匹配或不正确的字段和表名导致。

在大多数情况下,Rows.Scan 会复制它从驱动程序获取的数据,因为它不知道驱动程序如何重用其缓冲区。可以使用特殊类型 sql.RawBytes 来从驱动程序实际返回的数据中获取零拷贝的字节切片。在下次调用 Next() 之后,这样的值将不再有效,因为驱动程序可能已经覆盖了那段内存。

Query 使用的连接在通过 Next 迭代完所有行之前或调用 rows.Close() 之后一直保持活动状态,之后该连接将被释放。有关更多信息,请参阅关于连接池的部分。

sqlx 扩展的 Queryx 行为与 Query 完全一样,但返回的是 sqlx.Rows,它具有扩展的扫描行为:

1
2
3
4
5
6
7
8
9
10
11
type Place struct {
Country string
City sql.NullString
TelephoneCode int `db:"telcode"`
}
rows, err := db.Queryx("SELECT * FROM place")
for rows.Next() {
var p Place
err = rows.StructScan(&p)
}

sqlx.Rows 的主要扩展方法是 StructScan(),它可以自动将查询结果扫描到结构体的字段中。请注意,为了让 sqlx 能够写入这些字段,这些字段必须是导出的(即首字母大写),这是 Go 中所有序列化器(marshaller)的共同要求。你可以使用 db 结构标签来指定哪个列名映射到结构体的哪个字段,或者使用 db.MapperFunc() 设置新的默认映射规则。默认行为是使用 strings.ToLower 对字段名进行小写转换以匹配列名。有关 StructScanSliceScanMapScan 的更多信息,请参阅高级扫描部分。

查询单行 QueryRow

QueryRow 从服务器获取一行数据。它从连接池中获取一个连接,并使用 Query 执行查询,返回一个 Row 对象,该对象具有自己的内部 Rows 对象:

1
2
3
row := db.QueryRow("SELECT * FROM place WHERE telcode=?", 852)
var telcode int
err = row.Scan(&telcode)

Query 不同,QueryRow 返回一个 Row 类型的结果而不返回错误,这使得可以安全地从返回结果中链式调用 Scan 方法。如果执行查询时发生错误,该错误将由 Scan 返回。如果没有行,Scan 会返回 sql.ErrNoRows。如果扫描本身失败(例如,由于类型不匹配),也会返回该错误。

Row 结果内部的 Rows 结构在 Scan 时会被关闭,这意味着 QueryRow 使用的连接会在结果被扫描之前一直保持打开状态。这也意味着 sql.RawBytes 在这里不可用,因为引用的内存属于驱动程序,在控制权返回给调用者时可能已经无效。

sqlx 扩展的 QueryRowx 将返回一个sqlx.Row 而不是 sql.Row,它实现了与 Rows 相同的扫描扩展,上面已经说过,并在高级扫描部分有详细解释:

1
2
var p Place
err := db.QueryRowx("SELECT city, telcode FROM place LIMIT 1").StructScan(&p)

Get 和 Select

GetSelect 是针对 handler 类型的省时的扩展,它们将查询执行与灵活的扫描语义结合起来。为了清楚地解释它们,我们需要谈谈什么是可扫描的:

  • 如果一个值不是结构体,比如字符串(string)、整数(int),那么它就是可扫描的。
  • 如果一个值实现了 sql.Scanner 接口,那么它就是可扫描的。
  • 如果一个值是结构体,但没有导出的字段(例如 time.Time),那么它也是可扫描的。

GetSelect 在可扫描类型上使用 rows.Scan,在非可扫描类型上使用 rows.StructScan。它们大致分别对应于 QueryRowQuery,其中 Get 用于获取单个结果并进行扫描,而 Select 用于获取结果的切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
p := Place{}
pp := []Place{}
//这将直接将第一个地点拉取到p中
err = db.Get(&p, "SELECT * FROM place LIMIT 1")
// 这将把telcode大于50的地点拉取到切片pp中
err = db.Select(&pp, "SELECT * FROM place WHERE telcode > ?", 50)
// 也可以使用普通类型
var id int
err = db.Get(&id, "SELECT count(*) FROM place")
// 获取最多10个地点名称
var names []string
err = db.Select(&names, "SELECT name FROM place LIMIT 10")

这两个方法基本可以把我前一篇封装的helper函数替代掉了。

GetSelect 都会在查询执行过程中关闭它们创建的 Rows,并返回在此过程中任何步骤遇到的错误。由于它们内部使用 StructScan,因此高级扫描部分中的细节也适用于 GetSelect

Select 可以为您节省大量输入,但要小心!它在语义上与 Queryx 不同,因为它会一次性将整个结果集加载到内存中。如果查询没有将结果集限制在合理的大小,那么最好使用经典的 Queryx/StructScan 迭代方式。

试想你要处理几千万行的数据,一条一条的拉取和处理,比一次性读入到内存中处理,资源使用更友好。

事务 Transaction

要使用事务,您必须使用 DB.Begin() 创建一个事务 handler 。像这样的代码将不会工作:

1
2
3
4
// 这将不会工作,如果连接池>1
db.MustExec("BEGIN;")
db.MustExec(...)
db.MustExec("COMMIT;")

请记住,Exec 和其他所有查询动词每次都会向数据库请求一个连接,并在使用后将其返回给连接池。因此,无法保证您会收到执行 BEGIN 语句时使用的同一个连接。要使用事务,您必须使用DB.Begin()

1
2
3
tx, err := db.Begin()
err = tx.Exec(...)
err = tx.Commit()

DB handler 还有 Beginx()MustBegin() 扩展方法,它们返回一个 sqlx.Tx 而不是 sql.Tx

1
2
3
tx := db.MustBegin()
tx.MustExec(...)
err = tx.Commit()

sqlx.Tx 拥有 sqlx.DB 的所有 handler 扩展。

由于事务是连接状态,Tx 对象必须从连接池中绑定并控制一个单一的连接。在整个生命周期中,Tx 将维持这个单一的连接,只有在调用 Commit()Rollback() 时才会释放它。你应该至少调用这两个函数之一,否则连接将一直被占用,直到垃圾收集器回收。

因为在一个事务中你只能使用一个连接,所以你一次只能执行一个语句;在执行另一个查询之前,必须分别扫描或关闭 RowRows 类型的游标。如果你尝试在服务器向你发送结果时向服务器发送数据,它可能会破坏连接。

最后,Tx 对象并不实际上在服务器上执行任何行为;它们只是执行一个 BEGIN 语句并绑定一个单一的连接。事务的实际行为,包括锁定和隔离等,完全是未指定的,并且依赖于数据库。

预编译语句 Prepared Statement

在大多数数据库中,当执行查询时,实际上会在幕后准备语句。但是,您也可以使用 sqlx.DB.Prepare() 明确地准备语句以便在其他地方重用。

1
2
3
4
5
6
stmt, err := db.Prepare(`SELECT * FROM place WHERE telcode=?`)
row = stmt.QueryRow(65)
tx, err := db.Begin()
txStmt, err := tx.Prepare(`SELECT * FROM place WHERE telcode=?`)
row = txStmt.QueryRow(852)

Prepare 实际上是在数据库上执行准备操作的,因此它需要一个连接和连接状态。database/sql 为你抽象了这些,允许你通过在新连接上自动创建语句,从单个 Stmt 对象在多个连接上并发执行。Preparex() 返回一个 sqlx.Stmt,它拥有 sqlx.DBsqlx.Tx的所有 handler 扩展功能:

1
2
3
stmt, err := db.Preparex(`SELECT * FROM place WHERE telcode=?`)
var p Place
err = stmt.Get(&p, 852)

标准的 sql.Tx 对象还有一个 Stmt() 方法,该方法可以从预先存在的语句中返回一个特定于事务的语句。sqlx.Tx 有一个 Stmtx 版本,可以从现有的 sql.Stmtsqlx.Stmt 创建一个新的特定于事务的 sqlx.Stmt

查询辅助方法 Query Helper

database/sql 包不会对您的实际查询文本进行任何处理。这使得在您的代码中使用特定于后端的特性变得轻而易举;您可以像在数据库提示符中一样编写查询。虽然这非常灵活,但它使得编写某些类型的查询变得困难。

"In" 子句

由于 database/sql 不会检查您的查询,而是直接将参数传递给驱动程序,因此处理带有 IN 子句的查询会变得困难:

1
SELECT * FROM users WHERE level IN (?);

当在后端将其准备为语句时,绑定变量 ? 只会对应一个参数,但通常我们希望它根据某个切片的长度来对应可变数量的参数,例如:

1
2
var levels = []int{4, 6, 7}
rows, err := db.Query("SELECT * FROM users WHERE level IN (?);", levels)

通过使用 sqlx.In 预先处理查询语句,可以实现这种模式:

1
2
3
4
5
6
var levels = []int{4, 6, 7}
query, args, err := sqlx.In("SELECT * FROM users WHERE level IN (?);", levels)
// sqlx.In返回带有`?`绑定变量的查询,我们可以重新绑定它以适应我们的后端
query = db.Rebind(query)
rows, err := db.Query(query, args...)

使用 sqlx.In 预先处理查询语句可以实现这种模式:sqlx.In 会扩展传递给它的查询中的任何绑定变量(bindvars),这些绑定变量对应于参数中的切片,并扩展到与切片长度相同数量的占位符,然后将这些切片元素追加到一个新的参数列表中。它仅对 ? 绑定变量执行此操作;您可以使用 db.Rebind 来获取适合您后端的查询语句。

命名查询 Named Query

命名查询在许多其他数据库包中都很常见。它们允许您使用绑定变量语法,该语法通过结构体字段的名称或映射键来绑定查询中的变量,而不是按位置引用所有内容。结构体字段的命名约定遵循 StructScan 的规则,使用 NameMapperdb 结构体标签。与命名查询相关的有两个额外的查询动词:

  • NamedQuery(...) (*sqlx.Rows, error) - 类似于 Queryx,但使用命名绑定变量
  • NamedExec(...) (sql.Result, error) - 类似于 Exec,但使用命名绑定变量

还有一个额外的 handler 类型:

  • NamedStmt - 一个 sqlx.Stmt,可以使用命名绑定变量进行准备
1
2
3
4
5
6
7
// 使用结构体的命名查询
p := Place{Country: "South Africa"}
rows, err := db.NamedQuery(`SELECT * FROM place WHERE country=:country`, p)
// 使用map的命名查询
m := map[string]interface{}{"city": "Johannesburg"}
result, err := db.NamedExec(`SELECT * FROM place WHERE city=:city`, m)

命名查询的执行和准备适用于结构体和Map。如果你想要完整的查询操作集,可以准备一个命名语句并使用它:

1
2
3
4
5
6
p := Place{TelephoneCode: 50}
pp := []Place{}
// 查询所有telcode大于50的地点
nstmt, err := db.PrepareNamed(`SELECT * FROM place WHERE telcode > :telcode`)
err = nstmt.Select(&pp, p)

命名查询支持是通过解析查询中的 :param 语法,并将其替换为底层数据库支持的绑定变量来实现的,然后在执行时执行映射,因此它可以在 sqlx 支持的任何数据库上使用。你还可以使用 sqlx.Named,它使用 ? 绑定变量,并且可以与 sqlx.In 组合使用:

1
2
3
4
5
6
7
8
arg := map[string]interface{}{
"published": true,
"authors": []{8, 19, 32, 44},
}
query, args, err := sqlx.Named("SELECT * FROM articles WHERE published=:published AND author_id IN (:authors)", arg)
query, args, err := sqlx.In(query, args...)
query = db.Rebind(query)
db.Query(query, args...)

高级扫描 Advanced Scanning

StructScan 相当复杂但具有欺骗性。它支持嵌入的结构体,并使用与 Go 用于嵌入属性和方法访问相同的优先级规则为字段赋值。这种用法的一个常见例子是在多个表之间共享表模型的公共部分,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type AutoIncr struct {
ID uint64
Created time.Time
}
type Place struct {
Address string
AutoIncr
}
type Person struct {
Name string
AutoIncr
}

使用上述结构体,PersonPlace 都能够从 StructScan 中接收 idcreated 列,因为它们都嵌入了定义了这些列的 AutoIncr 结构体。这个功能可以让你快速地为连接操作创建一个临时的表。它还可以递归地工作;以下结构体可以通过 Go 的点运算符和 StructScan 访问 PersonName 字段、AutoIncrIDCreated 字段:

1
2
3
4
5
type Employee struct {
BossID uint64
EmployeeID uint64
Person
}

请注意,sqlx 历史上一度支持此功能用于非嵌入结构体,但这最终变得令人困惑,因为用户使用此功能来定义关系,并两次嵌入相同的结构体:

1
2
3
4
type Child struct {
Father Person
Mother Person
}

这会引起一些问题。在 Go 语言中,隐藏后代字段是合法的;如果嵌入示例中的 Employee 定义了一个 Name 字段,那么它会优先于 PersonName 字段。但是,模糊的选择器是非法的,并且会导致运行时错误。如果我们想为 PersonPlace 创建一个快速的 JOIN 类型,那么我们应该在哪里放置 id 列,这两个类型都通过嵌入的 AutoIncr 定义了 id 列?是否会出现错误?

由于 sqlx 构建字段名到字段地址映射的方式,在将结果扫描到结构体时,它不再知道在遍历结构体树时是否遇到过两次相同的字段名。因此,与 Go 语言不同,StructScan 会选择遇到的第一个具有该名称的字段。由于 Go 语言的结构体字段是从上到下排序的,而 sqlx 为了保持优先级规则,采用广度优先遍历,因此会选择最浅、最顶部的定义。例如,在以下类型中:

1
2
3
4
type PersonPlace struct {
Person
Place
}

StructScan 会将 id 列的结果设置在 Person.AutoIncr.ID 中,也可以通过 Person.ID 访问。为了避免混淆,建议你在 SQL 中使用 AS 来创建列别名。

安全扫描目的字段

默认情况下,如果某一列无法映射到目标结构体中的字段,StructScan 将返回一个错误。这模仿了 Go 中对未使用变量的处理方式,但与标准库中的序列化器(如 encoding/json)的行为不符。由于 SQL 通常以比解析 JSON 更受控的方式执行,并且这些错误通常是编码错误,因此决定默认返回错误。

与未使用的变量类似,忽略的列会浪费网络和数据库资源,而且在没有映射器通知未找到某些内容的情况下,很难在早期检测到不兼容的映射或结构标签中的拼写错误。

尽管如此,在某些情况下,可能希望忽略没有目标字段的列。为此,每种 Handle 类型都有一个 Unsafe 方法,它返回该 handler 的新副本,并关闭此安全检查:

1
2
3
4
5
6
7
var p Person
// 由于place列没有字段目标,所以这里的err不是nil
err = db.Get(&p, "SELECT * FROM person, place LIMIT 1;")
// 这不会返回错误,即使place列没有目标
udb := db.Unsafe()
err = udb.Get(&p, "SELECT * FROM person, place LIMIT 1;")

控制命名映射

用作 StructScan 目标的结构体字段必须大写以便 sqlx 能够访问。因此,sqlx 使用了一个 NameMapper,该映射器将字段名应用 strings.ToLower 函数以将它们映射到行结果中的列。但是,这并不总是符合需求的,这取决于你的数据库模式,因此 sqlx 允许以多种方式自定义映射。

最简单的方式是通过使用 sqlx.DB.MapperFunc 为数据库 handler 设置映射器,该方法接收一个类型为 func(string) string 的参数。如果你的库需要特定的映射器,并且你不想污染你接收到的 sqlx.DB,你可以为库创建一个副本以确保使用特定的默认映射:

1
2
3
4
5
6
// 如果我们的数据库模式使用大写列,我们可以使用普通字段
db.MapperFunc(strings.ToUpper)
// 假定一个库使用小写列,我们可以创建一个副本
copy := sqlx.NewDb(db.DB, db.DriverName())
copy.MapperFunc(strings.ToLower)

每个 sqlx.DB 使用 sqlx/reflectx 包的 Mapper 来实现这种映射,并将活动的映射器公开为 sqlx.DB.Mapper。你可以通过直接设置来进一步自定义 DB 上的映射:

1
2
3
4
import "github.com/jmoiron/sqlx/reflectx"
// 创建一个新的映射器,它将使用结构字段标签“json”而不是“db”
db.Mapper = reflectx.NewMapperFunc("json", strings.ToLower)

替代扫描类型

除了使用 ScanStructScan`,sqlxRowRows` 还可以用于自动返回结果切片或Map:

1
2
3
4
5
6
7
8
9
10
11
rows, err := db.Queryx("SELECT * FROM place")
for rows.Next() {
// cols 代表所有列结果的[]interface{}
cols, err := rows.SliceScan()
}
rows, err := db.Queryx("SELECT * FROM place")
for rows.Next() {
results := make(map[string]interface{})
err = rows.MapScan(results)
}

SliceScan 返回一个 []interface{},其中包含所有列的数据,这在你代表第三方执行查询且无法知道可能会返回哪些列的情况下非常有用。MapScan 的行为类似,但它将列名映射到 interface{} 类型的值上。这里有一个重要的注意事项:rows.Columns()返回的结果不包括完全限定的名称,因此执行如 SELECT a.id, b.id FROM a NATURAL JOIN b 的查询时,Columns 的结果将是 []string{"id", "id"},这会导致你的Map中其中一个结果会被覆盖。

自定义类型

上面的例子都使用了内置类型来进行扫描和查询,但 database/sql 提供了接口,允许你使用任何自定义类型:

  • sql.Scanner 允许你在 Scan() 中使用自定义类型
  • driver.Valuer 允许你在 Query/QueryRow/Exec 中使用自定义类型

这些是标准接口,使用它们可以确保与任何可能在 database/sql 之上提供服务的库的兼容性。要详细了解如何使用它们,请阅读这篇博客文章或查看 sqlx/types 包,该包实现了一些标准的有用类型。

连接池

语句准备和查询执行需要一个连接,DB 对象将管理一个连接池,以便它可以安全地用于并发查询。在 Go 1.2 及更高版本中,有两种方式控制连接池的大小:

  • DB.SetMaxIdleConns(n int)
  • DB.SetMaxOpenConns(n int)

默认情况下,连接池会无限制地增长,并且当池中没有空闲连接可用时,就会创建新的连接。你可以使用 DB.SetMaxOpenConns 来设置池的最大大小。未被使用的连接会被标记为空闲状态,如果不再需要,它们就会被关闭。为了避免频繁地创建和关闭连接,请使用 DB.SetMaxIdleConns 将最大空闲大小设置为适合你的查询负载的大小。

如果不小心持有连接,很容易遇到麻烦。为了避免这种情况:

  • 确保你使用 Scan() 扫描每个 Row 对象
  • 确保你通过 Close() 或完全迭代 Next() 来处理每个 Rows 对象
  • 确保每个事务都通过 Commit()Rollback() 返回其连接

如果你忽略了这些操作中的任何一个,它们所使用的连接可能会被保持到垃圾回收,而你的数据库将最终创建大量连接以补偿正在使用的连接。请注意,Rows.Close() 可以安全地多次调用,因此不必担心在可能不必要的地方调用它。