- [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 root,embeded 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不再被需要时,应当调用该方法,无需考虑其是否已经被调用过。