sqlx是一个用于扩展标准库database/sql的库,它提供了一些额外的功能,使得在Go中使用sql更加方便。sqlx的目标是保持database/sql的简单性,同时提供更多的功能。sqlx
为 Go 的标准 database/sql
库提供了一组扩展。sqlx 中的 sql.Conn
、sql.DB
、sql.TX
、sql.Stmt
、sql.Rows
、sql.Row
等版本都保留了底层接口不变,因此它们的接口是标准接口的超集。这使得将使用 database/sql
的现有代码库与 sqlx
集成相对容易。
主要的额外概念有:
- 将行映射到结构体(支持嵌入结构体)、Map和切片
- 支持命名参数,包括预编译语句
- 使用
Get
和Select
快速从查询到结构体/切片
sqlx
的目的是无缝地封装 database/sql,并提供在开发数据库驱动的应用程序时有用的便捷方法。它不会改变任何底层的 database/sql
方法。相反,所有扩展行为都是通过在包装类型上定义的新方法来实现的。
比如:
|
|
可以看到,它的核心类型都是对标准库的封装,然后在此基础上提供了更多的功能。
它是2013年发布,已经有11年的历史了,也许为了保持兼容,它没有对泛型提供支持,甚至interface{}
也没有改为any
,还支持Go 1.10的版本。
本文假定你已经有了Go开发数据库程序的基础。如果你还不了解,建议你阅读下面的材料:
本文是编译自作者写的sqlx图解指南。
引入sqlx
库以及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 :
|
|
在某些情况下,你可能希望同时打开数据库并建立连接,例如,在初始化阶段捕获配置问题。你可以使用 Connect
方法一次性完成这个操作,它会打开一个新的数据库并尝试进行 Ping
操作。MustConnect
变种在遇到错误时会触发 panic
,适合在你的包的模块级别使用:
|
|
基本查询
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
Exec
和 MustExec
从连接池中获取一个连接,并在服务器上执行提供的语句。对于不支持即席(ad-hoc)查询执行的驱动程序,可能会在幕后创建一个预处理语句来执行。在返回结果之前,连接会被返回到连接池中。
|
|
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 语句的结构。例如,使用绑定变量来尝试参数化列名或表名将无法工作:
|
|
查询 Query
Query
是使用 database/sql
执行查询并返回行结果的主要方法。Query
返回一个 sql.Rows
对象和一个错误:
|
|
你应该把 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
,它具有扩展的扫描行为:
|
|
sqlx.Rows
的主要扩展方法是 StructScan()
,它可以自动将查询结果扫描到结构体的字段中。请注意,为了让 sqlx
能够写入这些字段,这些字段必须是导出的(即首字母大写),这是 Go 中所有序列化器(marshaller
)的共同要求。你可以使用 db
结构标签来指定哪个列名映射到结构体的哪个字段,或者使用 db.MapperFunc()
设置新的默认映射规则。默认行为是使用 strings.ToLower
对字段名进行小写转换以匹配列名。有关 StructScan
、SliceScan
和 MapScan
的更多信息,请参阅高级扫描部分。
查询单行 QueryRow
QueryRow
从服务器获取一行数据。它从连接池中获取一个连接,并使用 Query
执行查询,返回一个 Row
对象,该对象具有自己的内部 Rows
对象:
|
|
与 Query
不同,QueryRow
返回一个 Row
类型的结果而不返回错误,这使得可以安全地从返回结果中链式调用 Scan
方法。如果执行查询时发生错误,该错误将由 Scan
返回。如果没有行,Scan
会返回 sql.ErrNoRows
。如果扫描本身失败(例如,由于类型不匹配),也会返回该错误。
Row
结果内部的 Rows
结构在 Scan
时会被关闭,这意味着 QueryRow
使用的连接会在结果被扫描之前一直保持打开状态。这也意味着 sql.RawBytes
在这里不可用,因为引用的内存属于驱动程序,在控制权返回给调用者时可能已经无效。
sqlx
扩展的 QueryRowx
将返回一个sqlx.Row
而不是 sql.Row
,它实现了与 Rows
相同的扫描扩展,上面已经说过,并在高级扫描部分有详细解释:
|
|
Get 和 Select
Get
和 Select
是针对 handler 类型的省时的扩展,它们将查询执行与灵活的扫描语义结合起来。为了清楚地解释它们,我们需要谈谈什么是可扫描的:
- 如果一个值不是结构体,比如字符串(
string
)、整数(int
),那么它就是可扫描的。 - 如果一个值实现了
sql.Scanner
接口,那么它就是可扫描的。 - 如果一个值是结构体,但没有导出的字段(例如
time.Time
),那么它也是可扫描的。
Get
和 Select
在可扫描类型上使用 rows.Scan
,在非可扫描类型上使用 rows.StructScan
。它们大致分别对应于 QueryRow
和 Query
,其中 Get
用于获取单个结果并进行扫描,而 Select
用于获取结果的切片:
|
|
这两个方法基本可以把我前一篇封装的helper函数替代掉了。
Get
和 Select
都会在查询执行过程中关闭它们创建的 Rows
,并返回在此过程中任何步骤遇到的错误。由于它们内部使用 StructScan
,因此高级扫描部分中的细节也适用于 Get
和 Select
。
Select
可以为您节省大量输入,但要小心!它在语义上与 Queryx
不同,因为它会一次性将整个结果集加载到内存中。如果查询没有将结果集限制在合理的大小,那么最好使用经典的 Queryx/StructScan
迭代方式。
试想你要处理几千万行的数据,一条一条的拉取和处理,比一次性读入到内存中处理,资源使用更友好。
事务 Transaction
要使用事务,您必须使用 DB.Begin()
创建一个事务 handler 。像这样的代码将不会工作:
|
|
请记住,Exec
和其他所有查询动词每次都会向数据库请求一个连接,并在使用后将其返回给连接池。因此,无法保证您会收到执行 BEGIN
语句时使用的同一个连接。要使用事务,您必须使用DB.Begin()
。
|
|
DB
handler 还有 Beginx()
和 MustBegin()
扩展方法,它们返回一个 sqlx.Tx
而不是 sql.Tx
:
|
|
sqlx.Tx
拥有 sqlx.DB
的所有 handler 扩展。
由于事务是连接状态,Tx
对象必须从连接池中绑定并控制一个单一的连接。在整个生命周期中,Tx
将维持这个单一的连接,只有在调用 Commit()
或 Rollback()
时才会释放它。你应该至少调用这两个函数之一,否则连接将一直被占用,直到垃圾收集器回收。
因为在一个事务中你只能使用一个连接,所以你一次只能执行一个语句;在执行另一个查询之前,必须分别扫描或关闭 Row
和 Rows
类型的游标。如果你尝试在服务器向你发送结果时向服务器发送数据,它可能会破坏连接。
最后,Tx
对象并不实际上在服务器上执行任何行为;它们只是执行一个 BEGIN
语句并绑定一个单一的连接。事务的实际行为,包括锁定和隔离等,完全是未指定的,并且依赖于数据库。
预编译语句 Prepared Statement
在大多数数据库中,当执行查询时,实际上会在幕后准备语句。但是,您也可以使用 sqlx.DB.Prepare()
明确地准备语句以便在其他地方重用。
|
|
Prepare
实际上是在数据库上执行准备操作的,因此它需要一个连接和连接状态。database/sql
为你抽象了这些,允许你通过在新连接上自动创建语句,从单个 Stmt
对象在多个连接上并发执行。Preparex()
返回一个 sqlx.Stmt
,它拥有 sqlx.DB
和 sqlx.Tx
的所有 handler 扩展功能:
|
|
标准的 sql.Tx
对象还有一个 Stmt()
方法,该方法可以从预先存在的语句中返回一个特定于事务的语句。sqlx.Tx
有一个 Stmtx
版本,可以从现有的 sql.Stmt
或 sqlx.Stmt
创建一个新的特定于事务的 sqlx.Stmt
。
查询辅助方法 Query Helper
database/sql
包不会对您的实际查询文本进行任何处理。这使得在您的代码中使用特定于后端的特性变得轻而易举;您可以像在数据库提示符中一样编写查询。虽然这非常灵活,但它使得编写某些类型的查询变得困难。
"In" 子句
由于 database/sql
不会检查您的查询,而是直接将参数传递给驱动程序,因此处理带有 IN
子句的查询会变得困难:
|
|
当在后端将其准备为语句时,绑定变量 ? 只会对应一个参数,但通常我们希望它根据某个切片的长度来对应可变数量的参数,例如:
|
|
通过使用 sqlx.In 预先处理查询语句,可以实现这种模式:
|
|
使用 sqlx.In
预先处理查询语句可以实现这种模式:sqlx.In
会扩展传递给它的查询中的任何绑定变量(bindvars
),这些绑定变量对应于参数中的切片,并扩展到与切片长度相同数量的占位符,然后将这些切片元素追加到一个新的参数列表中。它仅对 ?
绑定变量执行此操作;您可以使用 db.Rebind
来获取适合您后端的查询语句。
命名查询 Named Query
命名查询在许多其他数据库包中都很常见。它们允许您使用绑定变量语法,该语法通过结构体字段的名称或映射键来绑定查询中的变量,而不是按位置引用所有内容。结构体字段的命名约定遵循 StructScan
的规则,使用 NameMapper
和 db
结构体标签。与命名查询相关的有两个额外的查询动词:
NamedQuery(...) (*sqlx.Rows, error)
- 类似于 Queryx,但使用命名绑定变量NamedExec(...) (sql.Result, error)
- 类似于 Exec,但使用命名绑定变量
还有一个额外的 handler 类型:
NamedStmt
- 一个sqlx.Stmt
,可以使用命名绑定变量进行准备
|
|
命名查询的执行和准备适用于结构体和Map。如果你想要完整的查询操作集,可以准备一个命名语句并使用它:
|
|
命名查询支持是通过解析查询中的 :param
语法,并将其替换为底层数据库支持的绑定变量来实现的,然后在执行时执行映射,因此它可以在 sqlx
支持的任何数据库上使用。你还可以使用 sqlx.Named
,它使用 ?
绑定变量,并且可以与 sqlx.In
组合使用:
|
|
高级扫描 Advanced Scanning
StructScan
相当复杂但具有欺骗性。它支持嵌入的结构体,并使用与 Go 用于嵌入属性和方法访问相同的优先级规则为字段赋值。这种用法的一个常见例子是在多个表之间共享表模型的公共部分,例如:
|
|
使用上述结构体,Person
和 Place
都能够从 StructScan
中接收 id
和 created
列,因为它们都嵌入了定义了这些列的 AutoIncr
结构体。这个功能可以让你快速地为连接操作创建一个临时的表。它还可以递归地工作;以下结构体可以通过 Go 的点运算符和 StructScan
访问 Person
的 Name
字段、AutoIncr
的 ID
和 Created
字段:
|
|
请注意,sqlx
历史上一度支持此功能用于非嵌入结构体,但这最终变得令人困惑,因为用户使用此功能来定义关系,并两次嵌入相同的结构体:
|
|
这会引起一些问题。在 Go 语言中,隐藏后代字段是合法的;如果嵌入示例中的 Employee
定义了一个 Name
字段,那么它会优先于 Person
的 Name
字段。但是,模糊的选择器是非法的,并且会导致运行时错误。如果我们想为 Person
和 Place
创建一个快速的 JOIN
类型,那么我们应该在哪里放置 id
列,这两个类型都通过嵌入的 AutoIncr
定义了 id
列?是否会出现错误?
由于 sqlx
构建字段名到字段地址映射的方式,在将结果扫描到结构体时,它不再知道在遍历结构体树时是否遇到过两次相同的字段名。因此,与 Go 语言不同,StructScan
会选择遇到的第一个具有该名称的字段。由于 Go 语言的结构体字段是从上到下排序的,而 sqlx
为了保持优先级规则,采用广度优先遍历,因此会选择最浅、最顶部的定义。例如,在以下类型中:
|
|
StructScan
会将 id
列的结果设置在 Person.AutoIncr.ID
中,也可以通过 Person.ID
访问。为了避免混淆,建议你在 SQL
中使用 AS
来创建列别名。
安全扫描目的字段
默认情况下,如果某一列无法映射到目标结构体中的字段,StructScan
将返回一个错误。这模仿了 Go 中对未使用变量的处理方式,但与标准库中的序列化器(如 encoding/json
)的行为不符。由于 SQL 通常以比解析 JSON 更受控的方式执行,并且这些错误通常是编码错误,因此决定默认返回错误。
与未使用的变量类似,忽略的列会浪费网络和数据库资源,而且在没有映射器通知未找到某些内容的情况下,很难在早期检测到不兼容的映射或结构标签中的拼写错误。
尽管如此,在某些情况下,可能希望忽略没有目标字段的列。为此,每种 Handle 类型都有一个 Unsafe 方法,它返回该 handler 的新副本,并关闭此安全检查:
|
|
控制命名映射
用作 StructScan
目标的结构体字段必须大写以便 sqlx
能够访问。因此,sqlx
使用了一个 NameMapper
,该映射器将字段名应用 strings.ToLower
函数以将它们映射到行结果中的列。但是,这并不总是符合需求的,这取决于你的数据库模式,因此 sqlx
允许以多种方式自定义映射。
最简单的方式是通过使用 sqlx.DB.MapperFunc
为数据库 handler 设置映射器,该方法接收一个类型为 func(string) string
的参数。如果你的库需要特定的映射器,并且你不想污染你接收到的 sqlx.DB
,你可以为库创建一个副本以确保使用特定的默认映射:
|
|
每个 sqlx.DB
使用 sqlx/reflectx
包的 Mapper
来实现这种映射,并将活动的映射器公开为 sqlx.DB.Mapper
。你可以通过直接设置来进一步自定义 DB 上的映射:
|
|
替代扫描类型
除了使用 Scan
和 StructScan`
,sqlx的
Row或
Rows` 还可以用于自动返回结果切片或Map:
|
|
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()
可以安全地多次调用,因此不必担心在可能不必要的地方调用它。