sqlx 是 Go 语言中一个流行的第三方包,它提供了对 Go 标准库 database/sql
的扩展,旨在简化和改进 Go 语言中使用 SQL 的体验,并提供了更加强大的数据库交互功能。sqlx
保留了 database/sql
接口不变,是 database/sql
的超集,这使得将现有项目中使用的 database/sql
替换为 sqlx
变得相当轻松。
本文重点讲解 sqlx
在 database/sql
基础上扩展的功能,对于 database/sql
已经支持的功能则不会详细讲解。如果你对 database/sql
不熟悉,可以查看我的另一篇文章《在 Go 中如何使用 database/sql 来操作数据库》。
安装
sqlx
安装方式同 Go 语言中其他第三方包一样:
1 | $ go get github.com/jmoiron/sqlx |
sqlx 类型设计
sqlx
的设计与 database/sql
差别不大,编码风格较为统一,参考 database/sql
标准库,sqlx
提供了如下几种与之对应的数据类型:
sqlx.DB
:类似于sql.DB
,表示数据库对象,可以用来操作数据库。sqlx.Tx
:类似于sql.Tx
,事务对象。sqlx.Stmt
:类似于sql.Stmt
,预处理 SQL 语句。sqlx.NamedStmt
:对sqlx.Stmt
的封装,支持具名参数。sqlx.Rows
:类似于sql.Rows
,sqlx.Queryx
的返回结果。sqlx.Row
:类似于sql.Row
,sqlx.QueryRowx
的返回结果。
以上类型与 database/sql
提供的对应类型在功能上区别不大,但 sqlx
为这些类型提供了更友好的方法。
准备
为了演示 sqlx
用法,我准备了如下 MySQL 数据库表:
1 | DROP TABLE IF EXISTS `user`; |
你可以使用 MySQL 命令行或图形化工具创建这张表。
连接数据库
使用 sqlx
连接数据库:
1 | package main |
在 sqlx
中我们可以通过以上 5 种方式连接数据库。
sqlx.Open
对标 sql.Open
方法,返回 *sqlx.DB
类型。
sqlx.MustOpen
与 sqlx.Open
一样会返回 *sqlx.DB
实例,但如果遇到错误则会 panic
。
sqlx.NewDb
支持从一个 database/sql
包的 *sql.DB
对象创建一个新的 *sqlx.DB
类型,并且需要指定驱动名称。
使用前 3 种方式连接数据库并不会立即与数据库建立连接,连接将会在合适的时候延迟建立。为了确保能够正常连接数据库,往往需要调用 db.Ping()
方法进行验证:
1 | ctx := context.Background() |
sqlx
提供的 sqlx.Connect
方法就是用来简化这一操作的,它等价于 sqlx.Open
+ db.Ping
两个方法,其定义如下:
1 | func Connect(driverName, dataSourceName string) (*DB, error) { |
sqlx.MustConnect
方法在 sqlx.Connect
方法的基础上,提供了遇到错误立即 panic
的功能。看到 sqlx.MustConnect
方法的定义你就明白了:
1 | func MustConnect(driverName, dataSourceName string) *DB { |
以后当你遇见 MustXxx
类似方法名时就应该想到,其功能往往等价于 Xxx
方法,不过在其内部实现中,遇到 error
不再返回,而是直接进行 panic
,这也是 Go 语言很多库中的惯用方法。
声明模型
我们定义一个 User
结构体来映射数据库中的 user
表:
1 | type User struct { |
User
结构体在这里可以被称为「模型」。
执行 SQL 命令
database/sql
包提供了 *sql.DB.Exec
方法来执行一条 SQL 命令,sqlx
对其进行了扩展,提供了 *sqlx.DB.MustExec
方法来执行一条 SQL 命令:
1 | func MustCreateUser(db *sqlx.DB) (int64, error) { |
这里使用 *sqlx.DB.MustExec
方法插入了一条 user
记录。
*sqlx.DB.MustExec
方法定义如下:
1 | func (db *DB) MustExec(query string, args ...interface{}) sql.Result { |
与前文介绍的 sqlx.MustOpen
方法一样,*sqlx.DB.MustExec
方法也会在遇到错误时直接 panic
,其内部调用的是 *sqlx.DB.Exec
方法。
执行 SQL 查询
database/sql
包提供了 *sql.DB.Query
和 *sql.DB.QueryRow
两个查询方法,其签名如下:
1 | func (db *DB) Query(query string, args ...any) (*Rows, error) |
sqlx
在这两个方法的基础上,扩展出如下两个方法:
1 | func (db *DB) Queryx(query string, args ...interface{}) (*Rows, error) |
这两个方法返回的类型正是前文 sqlx 类型设计 中提到的 sqlx.Rows
、sqlx.Row
类型。
下面来讲解下这两个方法如何使用。
Queryx
使用 *sqlx.DB.Queryx
方法查询记录如下:
1 | func QueryxUsers(db *sqlx.DB) ([]User, error) { |
*sqlx.DB.Queryx
方法签名虽然与 *sql.DB.Query
方法基本相同,但它返回类型 *sqlx.Rows
得到了扩展,其提供的 StructScan
方法能够方便的将查询结果直接扫描到 User
结构体,这极大的增加了便携性,我们再也不用像使用 *sql.Rows
提供的 Scan
方法那样挨个写出 User
的属性了。
QueryRowx
使用 *sqlx.DB.QueryRowx
方法查询记录如下:
1 | func QueryRowxUser(db *sqlx.DB, id int) (User, error) { |
*sqlx.Row
同样提供了 StructScan
方法将查询结果扫描到结构体。
另外,这里使用了链式调用的方式,在调用 db.QueryRowx()
之后直接调用了 .StructScan(&u)
,接收的 err
是 StructScan
的返回结果。这是因为 db.QueryRowx()
的返回结果 *sqlx.Row
中记录了错误信息 err
,如果查询阶段遇到错误会被记录到 *sqlx.Row.err
中。在调用 StructScan
方法阶段,其内部首先判断 r.err != nil
,如果存在 err
直接返回错误,没有错误则将查询结果扫描到 dest
参数接收到的结构体指针,代码实现如下:
1 | type Row struct { |
sqlx
不仅扩展了 *sql.DB.Query
和 *sql.DB.QueryRow
两个查询方法,它还新增了两个查询方法:
1 | func (db *DB) Get(dest interface{}, query string, args ...interface{}) error |
*sqlx.DB.Get
方法包装了 *sqlx.DB.QueryRowx
方法,用以简化查询单条记录。
*sqlx.DB.Select
方法包装了 *sqlx.DB.Queryx
方法,用以简化查询多条记录。
接下来讲解这两个方法如何使用。
Get
使用 *sqlx.DB.Get
方法查询记录如下:
1 | func GetUser(db *sqlx.DB, id int) (User, error) { |
可以发现 *sqlx.DB.Get
方法用起来非常简单,我们不再需要调用 StructScan
方法将查询结果扫描到结构体中,只需要将结构体指针当作 Get
方法的第一个参数传递进去即可。
其代码实现如下:
1 | func (db *DB) Get(dest interface{}, query string, args ...interface{}) error { |
根据源码可以看出,*sqlx.DB.Get
内部调用了 *sqlx.DB.QueryRowx
方法。
Select
使用 *sqlx.DB.Select
方法查询记录如下:
1 | func SelectUsers(db *sqlx.DB) ([]User, error) { |
可以发现 *sqlx.DB.Select
方法用起来同样非常简单,它可以直接将查询结果扫描到 []User
切片中。
其代码实现如下:
1 | func (db *DB) Select(dest interface{}, query string, args ...interface{}) error { |
根据源码可以看出,*sqlx.DB.Select
内部调用了 *sqlx.DB.Queryx
方法。
sqlx.In
在 database/sql
中如果想要执行 SQL IN 查询,由于 IN 查询参数长度不固定,我们不得不使用 fmt.Sprintf
来动态拼接 SQL 语句,以保证 SQL 中参数占位符的个数是正确的。
sqlx
提供了 In
方法来支持 SQL IN 查询,这极大的简化了代码,也使得代码更易维护和安全。
使用示例如下:
1 | func SqlxIn(db *sqlx.DB, ids []int64) ([]User, error) { |
调用 sqlx.In
并传递 SQL 语句以及切片类型的参数,它将返回新的查询 SQL query
以及参数 args
,这个 query
将会根据 ids
来动态调整。
比如我们传递 ids
为 []int64{1, 2, 3}
,则得到 query
为 SELECT * FROM user WHERE id IN (?, ?, ?)
。
注意,我们接下来又调用 db.Rebind(query)
重新绑定了 query
变量的参数占位符。如果你使用 MySQL 数据库,这不是必须的,因为我们使用的 MySQL 驱动程序参数占位符就是 ?
。而如果你使用 PostgreSQL 数据库,由于 PostgreSQL 驱动程序参数占位符是 $n
,这时就必须要调用 db.Rebind(query)
方法来转换参数占位符了。
它会将 SELECT * FROM user WHERE id IN (?, ?, ?)
中的参数占位符转换为 PostgreSQL 驱动程序能够识别的参数占位符 SELECT * FROM user WHERE id IN ($1, $2, $3)
。
之后的代码就跟使用 database/sql
查询记录没什么两样了。
使用具名参数
sqlx
提供了两个方法 NamedExec
、NamedQuery
,它们能够支持具名参数 :name
,这样就不必再使用 ?
这种占位符的形式了。
这两个方法签名如下:
1 | func (db *DB) NamedExec(query string, arg interface{}) (sql.Result, error) |
其使用示例如下:
1 | func NamedExec(db *sqlx.DB) error { |
我们可以使用 :name
的方式来命名参数,它能够匹配 map
或 struct
对应字段的参数值,这样的 SQL 语句可读性更强。
事务
在事务的支持上,sqlx
扩展出了 Must
版本的事务,使用示例如下:
1 | func MustTransaction(db *sqlx.DB) error { |
不过这种用法不多,你知道就行。以下是事务的推荐用法:
1 |
|
我们使用 defer
语句来处理事务的回滚操作,这样就不必在每次处理错误时重复的编写调用 tx.Rollback()
的代码。
如果代码正常执行到最后,通过 tx.Commit()
来提交事务,此时即使再调用 tx.Rollback()
也不会对结果产生影响。
预处理语句
sqlx
针对 *sql.DB.Prepare
扩展出了 *sqlx.DB.Preparex
方法,返回 *sqlx.Stmt
类型。
*sqlx.Stmt
类型支持 Queryx
、QueryRowx
、Get
、Select
这些 sqlx
特有的方法。
其使用示例如下:
1 | func PreparexGetUser(db *sqlx.DB) (User, error) { |
*sqlx.DB.Preparex
方法定义如下:
1 | func Preparex(p Preparer, query string) (*Stmt, error) { |
实际上 *sqlx.DB.Preparex
内部还是调用的 *sql.DB.Preapre
方法,只不过将其返回结果构造成 *sqlx.Stmt
类型并返回。
不安全的扫描
在使用 *sqlx.DB.Get
等方法查询记录时,如果 SQL 语句查询出来的字段与要绑定的模型属性不匹配,则会报错。
示例如下:
1 | func GetUser(db *sqlx.DB) (User, error) { |
以上示例代码中,SQL 语句中查询了 id
、name
、email
、age
4 个字段,而 user
结构体则只有 ID
、Name
、Email
3 个属性,由于无法一一对应,执行以上代码,我们将得到如下报错信息:
1 | missing destination name age in *struct { ID int; Name string; Email string } |
这种表现是合理的,符合 Go 语言的编程风格,尽早暴露错误有助于减少代码存在 BUG 的隐患。
不过,有些时候,我们就是为了方便想要让上面的示例代码能够运行,可以这样做:
1 | func UnsafeGetUser(db *sqlx.DB) (User, error) { |
这里我们不再直接使用 db.Get
来查询记录,而是先通过 udb := db.Unsafe()
获取 unsafe
属性为 true
的 *sqlx.DB
对象,然后再调用它的 Get
方法。
*sqlx.DB
定义如下:
1 | type DB struct { |
当 unsafe
属性为 true
时,*sqlx.DB
对象会忽略不匹配的字段,使代码能够正常运行,并将能够匹配的字段正确绑定到 user
结构体对象上。
通过这个属性的名称我们就知道,这是不安全的做法,不被推荐。
与未使用的变量一样,被忽略的列是对网络和数据库资源的浪费,并且这很容易导致出现模型与数据库表不匹配而不被感知的情况。
Scan 变体
前文示例中,我们见过了 *sqlx.Rows.Scan
的变体 *sqlx.Rows.StructScan
的用法,它能够方便的将查询结果扫描到 struct
中。
sqlx
还提供了 *sqlx.Rows.MapScan
、*sqlx.Rows.SliceScan
两个方法,能够将查询结果分别扫描到 map
和 slice
中。
使用示例如下:
1 | func MapScan(db *sqlx.DB) ([]map[string]interface{}, error) { |
其中,rows.MapScan(r)
用法与 rows.StructScan(&u)
用法类似,都是将接收查询结果集的目标模型指针变量当作参数传递进来。
rows.SliceScan()
用法略有不同,它不接收参数,而是将结果保存在 []interface{}
中并返回。
可以按需使用以上两个方法。
控制字段名称映射
讲到这里,想必不少同学心里可能存在一个疑惑,rows.StructScan(&u)
在将查询记录的字段映射到对应结构体属性时,是如何找到对应关系的呢?
答案就是 db
结构体标签。
回顾前文讲 声明模型 时,User
结构体中定义的 CreatedAt
、UpdatedAt
两个字段,定义如下:
1 | CreatedAt time.Time `db:"created_at"` |
这里显式的标明了结构体标签 db
,sqlx
正是使用 db
标签来映射查询字段和模型属性。
默认情况下,结构体字段会被映射成全小写形式,如 ID
字段会被映射为 id
,而 CreatedAt
字段会被映射为 createdat
。
因为在 user
数据库表中,创建时间和更新时间两个字段分别为 created_at
、updated_at
,与 sqlx
默认字段映射规则不匹配,所以我才显式的为 CreatedAt
和 UpdatedAt
两个字段指明了 db
标签,这样 sqlx
的 rows.StructScan
就能正常工作了。
当然,数据库字段不一定都是小写,如果你的数据库字段为全大写,sqlx
提供了 *sqlx.DB.MapperFunc
方法来控制查询字段和模型属性的映射关系。
其使用示例如下:
1 | func MapperFuncUseToUpper(db *sqlx.DB) (User, error) { |
这里为了不改变原有的 db
对象,我们复制了一个 copyDB
,调用 copyDB.MapperFunc
并将 strings.ToUpper
传递进来。
注意这里的查询语句中,查询字段全部通过 as
重新命名成了大写形式,而 User
模型字段 db
默认都为小写形式。
copyDB.MapperFunc(strings.ToUpper)
的作用,就是在调用 Get
方法将查询结果扫描到结构体时,把 User
模型的小写字段,通过 strings.ToUpper
方法转成大写,这样查询字段和模型属性就全为大写了,也就能够一一匹配上了。
还有一种情况,如果你的模型已存在 json
标签,并且不想重复的再抄一遍到 db
标签,我们可以直接使用 json
标签来映射查询字段和模型属性。
1 | func MapperFuncUseJsonTag(db *sqlx.DB) (User, error) { |
这里需要直接修改 copyDB.Mapper
属性,赋值为 reflectx.NewMapperFunc("json", strings.ToLower)
将模型映射的标签由 db
改为 json
,并通过 strings.ToLower
方法转换为小写。
reflectx
按照如下方式导入:
1 | import "github.com/jmoiron/sqlx/reflectx" |
现在,查询语句中 name
属性通过使用 as
被重命名为 username
,而 username
刚好与 User
模型中 Name
字段的 json
标签相对应:
1 | Name sql.NullString `json:"username"` |
所以,以上示例代码能够正确映射查询字段和模型属性。
总结
sqlx
建立在 database/sql
包之上,用于简化和增强与关系型数据库的交互操作。
对常见数据库操作方法,sqlx
提供了 Must
版本,如 sqlx.MustOpen
用来连接数据库,*sqlx.DB.MustExec
用来执行 SQL 语句,当遇到 error
时将会直接 panic
。
sqlx
还扩展了查询方法 *sqlx.DB.Queryx
、*sqlx.DB.QueryRowx
、*sqlx.DB.Get
、*sqlx.DB.Select
,并且这些查询方法支持直接将查询结果扫描到结构体。
sqlx
为 SQL IN 操作提供了便捷方法 sqlx.In
。
为了使 SQL 更易阅读,sqlx
提供了 *sqlx.DB.NamedExec
、*sqlx.DB.NamedQuery
两个方法支持具名参数。
调用 *sqlx.DB.Unsafe()
方法能够获取 unsafe
属性为 true
的 *sqlx.DB
对象,在将查询结果扫描到结构体使可以用来忽略不匹配的记录字段。
除了能够将查询结果扫描到 struct
,sqlx
还支持将查询结果扫描到 map
和 slice
。
sqlx
使用 db
结构体标签来映射查询字段和模型属性,如果不显式指定 db
标签,默认映射的模型属性为小写形式,可以通过 *sqlx.DB.MapperFunc
函数来修改默认行为。
本文完整代码示例我放在了 GitHub 上,欢迎点击查看。
希望此文能对你有所帮助。
联系我
- 微信:jianghushinian
- 邮箱:jianghushinian007@outlook.com
- 博客地址:https://jianghushinian.cn/
参考