302 lines
14 KiB
Markdown
302 lines
14 KiB
Markdown
# SQLX
|
||
## Getting Start with SQLite
|
||
为了安装sqlx和database driver,可以通过如下命令:
|
||
```bash
|
||
$ go get github.com/jmoiron/sqlx
|
||
$ go get github.com/mattn/go-sqlite3
|
||
```
|
||
|
||
## Handle Types
|
||
`sqlx`和`database/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`实例
|
||
|
||
```golang
|
||
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
|
||
|
||
```go
|
||
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
|
||
`Exec`和`MustExec`方法会从connection pool中获取连接并且执行提供的sql方法。
|
||
|
||
> 并且,在Exec向调用方返回`sql.Result`对象之前,连接将会被归还给连接池。
|
||
>
|
||
> `此时,server已经向client发送了query text的执行结果,在根据返回结果构建sql.Result对象之前,会将来凝结返回给连接池。`
|
||
|
||
```go
|
||
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语句的结构。例如,如下语句都是不被允许的:
|
||
```go
|
||
// 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,
|
||
```go
|
||
// 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行为进行了拓展:
|
||
```go
|
||
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` object,`Row object`内部包含了`Rows`对象
|
||
```go
|
||
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`相同的拓展,示例如下
|
||
```go
|
||
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`方法,使用示例如下:
|
||
```go
|
||
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")
|
||
```
|
||
|
||
`Get`和`Select`都会对Rows进行关闭,并且在执行遇到错误时会返回error。
|
||
|
||
> 但是,需要注意的是,Select会将整个result set都导入到内存中。如果结果集较大,最好使用传统的`Queryx`/`StructScan`迭代方式。
|
||
|
||
### Exec和Query在归还连接池上的差异
|
||
Exec操作和Query操作在归还连接到连接池的时机有所不同:
|
||
- `Exec`: `Exec`方法在`server返回执行结果给client之后`,`client根据返回结果构建并返回sql.Result之前`,将会将连接返回给连接池
|
||
- `Query`: `Query`方法和`Exec`方法不同,其返回信息中包含结果集,必须等待结果集`迭代完成`或`手动调用rows.Close`方法之后,才会归还连接给连接池
|
||
|
||
## Transactions
|
||
为了使用事务,必须通过`DB.Begin()`方法创建事务,如下代码将`不会起作用`:
|
||
```go
|
||
// 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;")`执行时所在的连接。
|
||
|
||
可以按照如下示例来使用事务:
|
||
```go
|
||
tx, err := db.Begin()
|
||
err = tx.Exec(...)
|
||
err = tx.Commit()
|
||
```
|
||
类似的,sqlx同样提供了`Beginx()`方法和`MustBegin`方法,其会返回`sqlx.Tx`而不是`sql.Tx`,示例如下:
|
||
```go
|
||
tx := db.MustBegin()
|
||
tx.MustExec(...)
|
||
err = tx.Commit()
|
||
```
|
||
由于事务是连接状态,Tx对象必须绑定并控制连接池中的一个连接。Tx对象将会在生命周期内维持该connection,仅当commit或rollback被调用时才将连接释放。
|
||
|
||
在对Tx对象进行使用时,应当确保调用`commit`或`rollback`中的一个方法,否则连接将会被持有,直至发生垃圾回收。
|
||
|
||
在事务的声明周期中,只会关联一个连接,且`在执行其他查询前,row和rows对应的cursor必须被scan或关闭`。
|
||
|
||
## PreparedStatement
|
||
可以通过`sqlx.DB.Prepare()`方法来对想要重用的statements进行prepare操作:
|
||
```go
|
||
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`来执行,`那么后续操作会从连接池中获取一个新的连接来执行,并不会自动加入已经开启的事务`。
|
||
|
||
```go
|
||
tx, _ = db.Begin()
|
||
// 错误操作,此处dml会从连接池获取新的连接来执行
|
||
db.Exec(xxxx)
|
||
// 正确操作,此处dml会在tx绑定的连接中执行
|
||
tx.Exec(xxxx)
|
||
```
|
||
|
||
对于PreparedStatement操作,如果想要将其和事务相关联,有如下两种使用方式:
|
||
- `如果statement此时尚未创建`,可以通过`tx.Prepare`来创建该连接
|
||
- `如果Steement此时已经创建`,那么可以通过`tx.Stmt(dbStmt)`来获取新的Stmt,新Stmt会和事务所属连接绑定,通过新Stmt执行dml可以加入当前事务
|
||
|