Files
rikako-note/Golang/go-sqlx.md
2025-06-21 20:42:45 +08:00

25 KiB
Raw Blame History

SQLX

Getting Start with SQLite

为了安装sqlx和database driver可以通过如下命令

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

Handle Types

sqlxdatabase/sql等价,主要有四种类型:

  • sqlx.DB: 和sql.DB等价用于表示database
  • sqlx.Tx: 和sql.Tx等价,用于表示事务
  • sqlx.Stmt: 和sql.Stmt等价用于表示Prepared Statement
  • sqlx.NamedStmt 用于表示Prepared Statement with support for named parameters

Handle Types中都嵌入了其database/sql中等价的对象,在调用sqlx.DB.query时,其实际调用的代码和sql.DB.Query

例如,sqlx.DB实现中包含了sql.DB的对象引用,在调用sqlx.DB的方法时,实际会调用内部嵌套的sql.DB中的方法。

sqlx中除了上述类型外还引入了两个cursor类型

  • sqlx.Rows 等价于sql.Rows,为Queryx返回的cursor
  • sqlx.Row 等价于sql.Row:,由QueryRowx返回的结果

在上述两个cursor类型中sqlx.Rows嵌入了sql.Rows但是由于底层实现不可访问sqlx.Row对sql.Row的部分内容进行了重新实现标准接口保持一致

Connecting to database

一个DB实例代表的只是一个数据库的抽象,其并不代表实际连接。故而对DB实例进行创建并不会返回error或是抛出panic

DB实例在内部维护了一个连接池并且初次需要获取连接时去尝试建立连接。

创建sqlx.DB有两种方式,

  • 通过sqlx.DB.Open方法进行创建
  • 通过sqlx.DB.NewDb方法进行创建,该方法接收一个已经存在的sql.DB实例
var db *sqlx.DB
 
// exactly the same as the built-in
db = sqlx.Open("sqlite3", ":memory:")
 
// from a pre-existing sql.DB; note the required driverName
db = sqlx.NewDb(sql.Open("sqlite3", ":memory:"), "sqlite3")
 
// force a connection and test that it worked
err = db.Ping()

Open Db and connect in same time

在某些场景下,可能希望在创建数据库实例时就连接数据库,可以使用如下方法,它们会在创建数据库的同时调用Ping

  • sqlx.Connect如果在遇到错误时会返回error
  • sqlx.MustConnect在遇到错误时会发生panic
var err error
// open and connect at the same time:
db, err = sqlx.Connect("sqlite3", ":memory:")
 
// open and connect at the same time, panicing on error
db = sqlx.MustConnect("sqlite3", ":memory:")

Quering

handle Types中实现了和database/sql中同样的database方法

  • Exec(...) (sql.Result, error)database/sql中一致
  • Query(...) (*sql.Rows, error):和database/sql中一致
  • QueryRow(...) *sqlRow:和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方法会从connection pool中获取连接并且执行提供的sql方法。

并且在Exec向调用方返回sql.Result对象之前,连接将会被归还给连接池。

此时server已经向client发送了query text的执行结果在根据返回结果构建sql.Result对象之前会将来凝结返回给连接池。

schema := `CREATE TABLE place (
    country text,
    city text NULL,
    telcode integer);`
 
// execute a query on the server
result, err := db.Exec(schema)
 
// or, you can use MustExec, which panics on error
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)

db.Exec返回的结果中包含两部分信息:

  • LastInsertedId: 在mysql中该字段在使用auto_increment key时会返回最后插入的id
  • RowsAffected

bindvars

上述示例中,?占位符在内部调用了bindvars使用占位符能够避免sql注入。

database/sql并不会对query text做任何验证其将query text和encoded params原封不动的发送给server。

除非底层driver做了实现否则query语句会在server端运行sql语句之前执行prepare操作bindvars每个database可能语法都不相同

  • mysql会使用?来作为bindvars的占位符
  • postgresql会使用$1, $2来作为bindvars的占位符
  • sqlite既接收?又接收$1
  • oracle接收:name语法
Rebind

可以通过sqlx.DB.Rebind(string) string方法,将使用?的query sql转化为当前数据库类型的query sql。

bindvars is only used for parameterization

bindvars机制只能够被用于参数化并不允许通过bindvars改变sql语句的结构。例如如下语句都是不被允许的

// doesn't work
db.Query("SELECT * FROM ?", "mytable")
 
// also doesn't work
db.Query("SELECT ?, ? FROM people", "name", "location")

Query

database/sql主要通过Query方法来执行查询语句并获取row results。Query方法会返回一个sql.Rows对象和一个error

// fetch all places from the db
rows, err := db.Query("SELECT country, city, telcode FROM place")
 
// iterate over each row
for rows.Next() {
    var country string
    // note that city can be NULL, so we use the NullString type
    var city    sql.NullString
    var telcode int
    err = rows.Scan(&country, &city, &telcode)
}
// check the error from rows
err = rows.Err()

在使用Rows时应当将其看作是database cursor而非是结果反序列化之后构成的列表。尽管驱动对结果集的缓存行为各不相同但是通过Next方法对Rows中的结果进行迭代仍然可以在result set较大的场景下节省内存的使用因为Next同时只会对一行结果进行扫描。

Scan方法会使用反射将column返回的结果类型映射为go类型例如string, []byte等

在使用Rows时,如果并不迭代完整个结果集,请确保调用rows.Close()方法将连接返回到连接池中。

Query errors

其中,Query方返回的error可能是在服务端进行prepare操作或execute操作时发生的任何异常,该异常的可能场景如下:

  • 从连接池中获取了bad connection
  • 因sql语法、类型不匹配、不正确的field name或table name导致的错误

在大多数场景下,Rows.Scan会复制其从driver获取的数据因为Rows.Scan并无法感知driver对缓冲区进行reuse的方式。类型sql.RawBytes可以被用于获取 zero-copy slice of bytes from the actual data returned by the driver。在下一次调用Next方法时该值将会无效因为该bytes的内存空间将会被driver重写。

Connection Closed Scenes

在调用完Query方法后connection将会等到如下两种场景才会关闭

  • Rows中所有的行都通过Next方法调用被迭代
  • rows中所有行未被完全迭代但是rows.Close()方法被调用

Queryx

sqlx拓展的Queryx方法,其行为和Query方法一致,但是实际返回的是sqlx.Rows类型,sqlx.Rows类型对scan行为进行了拓展

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()其会自动将结果扫描到struct fields中。

注意在使用struct scan时struct field必须被exported首字母大写

可以使用db struct tag指定field映射到哪个column。默认情况下会对field name使用strings.Lower并和column name相匹配。

QueryRow

QueryRow会从server端拉取一行数据。其从connection pool中获取一个连接并且通过Query执行该查询并返回一个Row objectRow object内部包含了Rows对象

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

和Query不同的是QueryRow并不会返回error故而可以对返回结果链式嵌套其他方法调用例如Scan

如果在执行QueryRow查询时报错那么该error将会被Scan方法返回如果并没有查询到rows那么Scan方法将会返回sql.ErrNoRows

如果Scan操作失败了例如类型不匹配error也会被Scan方法返回。

Row对象内部的Rows结构在Scan后就会关闭即代表QueryRow使用的连接持续打开直到Result被扫描后才会关闭

QueryRowx

QueryRowx拓展将会返回一个sqlx.Row,其实现了和sqlx.Rows`相同的拓展,示例如下

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

Get & Select

Get/Select和QueryRow/Query类似,但是其能够节省代码编写,并能提供灵活的扫描语义。

scannable

scannable定义如下

  • 如果value并不是struct那么该value是scannable的
  • 如果value实现了sql.Scanner那么该value是scannable的
  • 如果struct没有exported field那么其是scannble的

Get和Select对于scannable类型使用了rows.Scan方法对non-scannable类型使用rows.StructScan方法,使用示例如下:

p := Place{}
pp := []Place{}
 
// this will pull the first place directly into p
err = db.Get(&p, "SELECT * FROM place LIMIT 1")
 
// this will pull places with telcode > 50 into the slice pp
err = db.Select(&pp, "SELECT * FROM place WHERE telcode > ?", 50)
 
// they work with regular types as well
var id int
err = db.Get(&id, "SELECT count(*) FROM place")
 
// fetch at most 10 place names
var names []string
err = db.Select(&names, "SELECT name FROM place LIMIT 10")

GetSelect都会对Rows进行关闭并且在执行遇到错误时会返回error。

但是需要注意的是Select会将整个result set都导入到内存中。如果结果集较大最好使用传统的Queryx/StructScan迭代方式。

Exec和Query在归还连接池上的差异

Exec操作和Query操作在归还连接到连接池的时机有所不同

  • Exec Exec方法在server返回执行结果给client之后client根据返回结果构建并返回sql.Result之前,将会将连接返回给连接池
  • Query Query方法和Exec方法不同,其返回信息中包含结果集,必须等待结果集迭代完成手动调用rows.Close方法之后,才会归还连接给连接池
  • QueryRow在返回的Row对象被Scan后将会归还连接给数据库

Transactions

为了使用事务,必须通过DB.Begin()方法创建事务,如下代码将不会起作用

// this will not work if connection pool > 1
db.MustExec("BEGIN;")
db.MustExec(...)
db.MustExec("COMMIT;")

在通过Exec执行语句时,每次都是从数据库获取连接,并在执行完后将连接返还到连接池中。

连接池并不保证第二次Exec执行时获取的连接和第一次Exec时获取的来连接相同。可能db.MustExec("BEGIN;")在获取连接->执行->将连接返还连接池后,第二次调用db.MustExec(...)时,从数据库获取的连接并不是Must("BEGIN;")执行时所在的连接。

可以按照如下示例来使用事务:

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

类似的sqlx同样提供了Beginx()方法和MustBegin方法,其会返回sqlx.Tx而不是sql.Tx,示例如下:

tx := db.MustBegin()
tx.MustExec(...)
err = tx.Commit()

由于事务是连接状态Tx对象必须绑定并控制连接池中的一个连接。Tx对象将会在生命周期内维持该connection仅当commit或rollback被调用时才将连接释放。

在对Tx对象进行使用时应当确保调用commitrollback中的一个方法,否则连接将会被持有,直至发生垃圾回收。

在事务的声明周期中,只会关联一个连接,且在执行其他查询前row和rows对应的cursor必须被scan或关闭

PreparedStatement

可以通过sqlx.DB.Prepare()方法来对想要重用的statements进行prepare操作

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操作实际会在数据库运行故而其需要connection和connection state。

PreparedStatement事务操作

在使用sqlx进行事务操作时首先需要通过db.Begin()开启事务,且后续所有想要加入事务的操作dml/query都需要通过tx.Exec/tx.Query来执行

如果在开启事务并获取tx对象后,后续操作仍然通过db.Exec/db.Query来执行,那么后续操作会从连接池中获取一个新的连接来执行,并不会自动加入已经开启的事务

tx, _ = db.Begin()
// 错误操作此处dml会从连接池获取新的连接来执行
db.Exec(xxxx)
// 正确操作此处dml会在tx绑定的连接中执行
tx.Exec(xxxx)

对于PreparedStatement操作如果想要将其和事务相关联有如下两种使用方式

  • 如果statement此时尚未创建,可以通过tx.Prepare来创建该连接
  • 如果Statement此时已经创建,那么可以通过tx.Stmt(dbStmt)来获取新的Stmt新Stmt会和事务所属连接绑定通过新Stmt执行dml可以加入当前事务

QueryHelper

database/sql并不会对传入的实际query text做任何处理故而在编写部分语句时可能会较为困难。

In Queries

由于database/sql并不会对query text做为何处理而是直接将其传给driver故而在处理in查询语句时将变得相当困难:

SELECT * FROM users WHERE level IN (?);

此处,可以通过sqlx.In方法来首先进行处理,示例如下:

var levels = []int{4, 6, 7}
query, args, err := sqlx.In("SELECT * FROM users WHERE level IN (?);", levels)
 
// sqlx.In returns queries with the `?` bindvar, we can rebind it for our backend
query = db.Rebind(query)
rows, err := db.Query(query, args...)

sqlx.In会将query text中所有和args中slice类型argument相关联的bindvar拓展到slice的长度并且将args中的参数重新添加到新的argList中示例如下所示

var query string
var args []interface{}
query, args, err = sqlx.In("select * from location where cities in (?) and code = ? and id in (?)", []string{"BEIJING", "NEW_YORK"},"asahi", []uint64{1, 3})
if err != nil {
	panic(err)
}
query = db.Rebind(query)
log.Printf("query transformed: %s\n", query)
log.Printf("args transformed: %v, %v, %v, %v, %v\n", args[0], args[1], args[2], args[3], args[4])

其对应结果为

2025/06/21 16:30:38 query transformed: select * from location where cities in (?, ?) and code = ? and id in (?, ?)
2025/06/21 16:30:38 args transformed: BEIJING, NEW_YORK, asahi, 1, 3

其中,包含[]string{"BEIJING", "NEW_YORK"},"asahi", []uint64{1, 3})三个元素的args被重新转换为了包含BEIJING, NEW_YORK, asahi, 1, 3五个元素的argList。

Named Queries

可以使用named bindvar语法来将struct field/map keys和variable绑定在一起struct field命名遵循StructScan

关联方法如下:

  • NamedQuery(...) (*sqlx.Rows, error)和Queryx类似但是支持named bindvars
  • NamedExec(...) (sql.Result, error)和Exec类似但是支持named bindvars

除此之外,还有额外的类型:

  • NamedStmt: 和sqlx.Stmt类似,支持prepared with named bindvars
// named query with a struct
p := Place{Country: "South Africa"}
rows, err := db.NamedQuery(`SELECT * FROM place WHERE country=:country`, p)
 
// named query with a map
m := map[string]interface{}{"city": "Johannesburg"}
result, err := db.NamedExec(`SELECT * FROM place WHERE city=:city`, m)

Named query executionNamed preparation针对struct和map都起作用如果希望在所有的query verbs都支持named queries可以使用named statement

p := Place{TelephoneCode: 50}
pp := []Place{}
 
// select all telcodes > 50
nstmt, err := db.PrepareNamed(`SELECT * FROM place WHERE telcode > :telcode`)
err = nstmt.Select(&pp, p)

Named query会将:param语法转化为底层数据库支持的bindvar语法并且在执行时执行mapping故而其对sqlx支持的所有数据库都适用。

可以使用sqlx.Named方式来将:param语法转化为?形式,并后续和sqlx.In相结合,示例如下:

	var query string
	params := map[string]any{
		"code":   "ASAHI",
		"cities": []string{"BEIJING", "NEWYORK"},
		"id":     []uint64{1, 3},
	}
	var args []any
	query, args, err = sqlx.Named("select * from location where cities in (:cities) and code = :code and id in (:id)",
		params)
	if err != nil {
		log.Fatal(err.Error())
	}
	log.Printf("query after named transformation: %s\n", query)
	log.Printf("args after named transformation: %v\n", args)

上述的输出为

2025/06/21 16:58:39 query after named transformation: select * from location where cities in (?) and code = ? and id in (?)
2025/06/21 16:58:39 args after named transformation: [[BEIJING NEWYORK] ASAHI [1 3]]

其中,sqlx.Named会将struct/map参数转化为argList,并且将named query转化为query with bindvar syntax的形式,后续,query with bindvar syntax还可以结合sqlx.In来使用

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

Struct Scan支持embeded struct并且对embeded attribute和method access使用和golang相同的优先顺序

  • 即struct scan在执行过程中如果struct A嵌套了struct B并且A和B中都拥有名为ID的字段那么将query结果扫描到A中时query result中的id将会被优先映射到A中的ID字段中,而B.ID字段则会被忽略
type struct B {
    ID string
    xxx
}

type struct A {
    B
    ID string 
    xxx
}

a := A{}
db.Select(&a, "select id from table_a where xxx")
// 会被映射到A中名为ID的struct field中B.ID在struct scan时会被忽略

struct embeded

在日常使用中,通常会使用struct embeded通常用作将多张tables共享的公共field抽取到一个embeded struct中,示例如下:

type AutoIncr struct {
    ID       uint64
    Created  time.Time
}
 
type Place struct {
    Address string
    AutoIncr
}
 
type Person struct {
    Name string
    AutoIncr
}

上述示例中person和place都支持在struct scan时接收id和created字段。并且其是递归的

type Employee struct {
    BossID uint64
    EmployeeID uint64
    Person
}

上述示例中,Employee中包含了来自Person中的NameAutoIncr ID/Created字段。

在sqlx构建field name和field address的映射时在将数据扫描到struct之前无法知晓name是否会在struct tree中遭遇两次。

field name和field address的映射关系其表示如下

  • field name: 该struct field对应的name
  • field address: 该struct field对应的address

例如:

type A struct {
  ID int `db:"id"`    
}
其中field name为`id`而field address则为`&a.ID`所代表的地址

field name到field address的映射关系将会在后续structScan时使用

并不像go中ambiguous selectors会导致异常,struct Scan将会选择遇到的第一个拥有相同name的field。以最外层的struct作为tree rootembeded struct作为child root其遵循shallowest, top-most的匹配原则,即离root最近且在同一struct定义中定义靠上的struct field更优先

例如,在如下定义中:

type PersonPlace struct {
    Person
    Place
}

Scan Destination Safety

默认情况下如果column并没有匹配到对应的field将会返回一个error。

但是如果想要在column未匹配到field的情况下不返回error那么可以使用db.Unsafe方法,示例如下:

var p Person
// err here is not nil because there are no field destinations for columns in `place`
err = db.Get(&p, "SELECT * FROM person, place LIMIT 1;")
 
// this will NOT return an error, even though place columns have no destination
udb := db.Unsafe()
err = udb.Get(&p, "SELECT * FROM person, place LIMIT 1;")

Control Name Mapping

被用作target field的struct field首字母必须大写从而使该field可以被sqlx访问。

因为struct field的首字母为大写故而sqlx使用了NameMapper对field name执行了strings.ToLower方法并将toLower之后的fieldname和rows result相匹配。

sqlx除了上述默认的匹配方法外还支持自定义的匹配。

sqlx.DB.MapperFunc

最简单的自定义匹配的方式是使用sqlx.DB.MapperFunc,其接收一个func(string) string参数。使用示例如下:

// if our db schema uses ALLCAPS columns, we can use normal fields
db.MapperFunc(strings.ToUpper)
 
// suppose a library uses lowercase columns, we can create a copy
copy := sqlx.NewDb(db.DB, db.DriverName())
copy.MapperFunc(strings.ToLower)

Slice Scan & Map Scan

除了structScan之外sqlx还支持slice scan和map scan的形式示例如下

rows, err := db.Queryx("SELECT * FROM place")
for rows.Next() {
    // cols is an []interface{} of all of the column results
    cols, err := rows.SliceScan()
}
 
rows, err := db.Queryx("SELECT * FROM place")
for rows.Next() {
    results := make(map[string]interface{})
    err = rows.MapScan(results)
}

Scanner/Valuer

通过sql.Scanner/driver.Valuer接口可以实现自定义类型的读取/写入。

Connection Pool

db对象将会管理一个连接池有两种方式可以控制连接池的大小

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

默认情况下,连接池会无限增长,在任何时刻如果连接池中没有空闲的连接都会创建新连接。可以通过DB.SetMaxOpenConns(n int)来控制连接池中的最大连接数目。

如果连接池中的连接没有在使用那么会被标注为idle状态并且在不再被需要时关闭。为了避免频繁的关闭和创建连接可以使用DB.SetMaxIdleConns来设置最大的空闲连接数量,从而适配业务场景的查询负载。

为了避免长期持有连接,需要确保如下条目:

  • 确保针对每个Row Object执行了Scan操作
  • 确保对于Rows object要么调用了Close方法要么调用Next进行了完全迭代
  • 确保每个事务都调用了commit或rollback

如果上述描述中的某一项没有被保证,那么连接可能被长期持有直到发生垃圾回收,为了弥补被持有的连接,需要创建更多连接。

Rows.Close方法可以被安全的多次调用故而当rows object不再被需要时应当调用该方法无需考虑其是否已经被调用过。