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

578 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

- [SQLX](#sqlx)
- [Getting Start with SQLite](#getting-start-with-sqlite)
- [Handle Types](#handle-types)
- [Connecting to database](#connecting-to-database)
- [Open Db and connect in same time](#open-db-and-connect-in-same-time)
- [Quering](#quering)
- [Exec](#exec)
- [bindvars](#bindvars)
- [Rebind](#rebind)
- [bindvars is only used for parameterization](#bindvars-is-only-used-for-parameterization)
- [Query](#query)
- [Query errors](#query-errors)
- [Connection Closed Scenes](#connection-closed-scenes)
- [Queryx](#queryx)
- [QueryRow](#queryrow)
- [QueryRowx](#queryrowx)
- [Get \& Select](#get--select)
- [scannable](#scannable)
- [Exec和Query在归还连接池上的差异](#exec和query在归还连接池上的差异)
- [Transactions](#transactions)
- [PreparedStatement](#preparedstatement)
- [PreparedStatement事务操作](#preparedstatement事务操作)
- [QueryHelper](#queryhelper)
- [`In` Queries](#in-queries)
- [`Named Queries`](#named-queries)
- [Advanced Scanning](#advanced-scanning)
- [struct embeded](#struct-embeded)
- [Scan Destination Safety](#scan-destination-safety)
- [Control Name Mapping](#control-name-mapping)
- [sqlx.DB.MapperFunc](#sqlxdbmapperfunc)
- [Slice Scan \& Map Scan](#slice-scan--map-scan)
- [Scanner/Valuer](#scannervaluer)
- [Connection Pool](#connection-pool)
# 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`方法之后,才会归还连接给连接池
- `QueryRow`在返回的Row对象被Scan后将会归还连接给数据库
## 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`来创建该连接
- `如果Statement此时已经创建`,那么可以通过`tx.Stmt(dbStmt)`来获取新的Stmt新Stmt会和事务所属连接绑定通过新Stmt执行dml可以加入当前事务
## QueryHelper
`database/sql`并不会对传入的实际query text做任何处理故而在编写部分语句时可能会较为困难。
### `In` Queries
由于`database/sql`并不会对query text做为何处理而是直接将其传给driver故而在处理`in`查询语句时将变得相当困难:
```sql
SELECT * FROM users WHERE level IN (?);
```
此处,可以通过`sqlx.In`方法来首先进行处理,示例如下:
```golang
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中示例如下所示
```go
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])
```
其对应结果为
```go
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`
```go
// 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 execution`和`Named preparation`针对struct和map都起作用如果希望在所有的query verbs都支持named queries可以使用named statement
```go
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`相结合,示例如下:
```go
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)
```
上述的输出为
```go
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`来使用
```go
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`字段则会被忽略
```go
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中`,示例如下:
```go
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字段。并且`其是递归的`。
```go
type Employee struct {
BossID uint64
EmployeeID uint64
Person
}
```
上述示例中,`Employee`中包含了来自`Person`中的`Name`和`AutoIncr 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
>
> 例如:
> ```go
> 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更优先`。
例如,在如下定义中:
```go
type PersonPlace struct {
Person
Place
}
```
### Scan Destination Safety
默认情况下如果column并没有匹配到对应的field将会返回一个error。
但是如果想要在column未匹配到field的情况下不返回error那么可以使用`db.Unsafe`方法,示例如下:
```go
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`参数。使用示例如下:
```go
// 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的形式示例如下
```go
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不再被需要时应当调用该方法无需考虑其是否已经被调用过。