关于在Go中如何使用SQL:

如果你要从了解Go语言开始的话,可以参考:

因为sqlx会包含所有database/sql已有的接口,所以本文中所有关于database/sql的使用建议也同样适用于sqlx。

安装

开始之前,你需要安装sqlx和Go数据库驱动。建议从sqlite3开始入门:

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

handle类型

sqlx最大可能去实现database/sql一样的功能。有4中主要handle类型:

  • sqlx.DB - 相当于database/sql中的sql.DB,代表一个数据库。
  • sqlx.Tx - 相当于database/sql中的sql.Tx,代表一个事务。
  • sqlx.Stmt = 相当于database/sql中的sql.Stmt,代表一条要执行的语句。
  • sqlx.NamedStmt - 代表一条有参数的执行语句。

所有handle类型内嵌实现了对应database/sql中handle。也就是说,当你在代码中调用sqlx.DB.Query的时候,同时也会调用执行到sql.DB.Query对应的代码。

除此之外,还有两种指针类型:

  • sqlx.Rows - 相当于sql.Rows, 从Queryx返回的指针;
  • sqlx.Row - 相当于sql.Row, 从QueryRowx返回的结果;

跟handle类型一样,sqlx.Rows内嵌了sql.Rows。犹豫sql.Row的底层实现未公开,sqlx.Row只是实现了sql.Row的部分标准接口。

连接数据库

DB本身并不是一个数据库连接,而是数据库的一个抽象表示。这也是为什么单独创建DB的时候不会报错。DB内部维护了一个连接池,会根据需要自动尝试去建立连接

两种创建sqlx.DB的方式:

  • 1)通过Open创建;
  • 2)通过NewDB从现有的sql.DB创建。
var db *sqlx.DB
// 方式1:使用Open函数,跟使用内置的sql.Open一样
db = sqlx.Open("sqlite3", ":memory:")
// 方式2:使用NewDB从sql.DB创建,driverName必须指定
db = sqlx.NewDb(sql.Open("sqlite3", ":memory:"), "sqlite3")
// 建立并测试连接
err = db.Ping()

在某些情况下,可能需要创建数据库并同时建立连接。比如,在程序初始化阶段检测数据库配置问题,这时可以直接使用Connect函数,会新建一个DB,并执行ping检查连接;另外,模块执行阶段可以使用MustConnect,在连接错误时终止模块执行。

var err error
// 同时打开并连接数据库,返回错误信息
db, err = sqlx.Connect("sqlite3", ":memory:")
// 同时打开并连接数据库,遇到错误终止程序
db = sqlx.MustConnect("sqlite3", ":memory:")

查询 Query

sqlx中的handle类型实现了跟database/sql相同的数据库操作:

  • Exec(…) (sql.Result, error) - 跟database/sql中函数相同
  • Query(…) (*sql.Rows, error) - 跟database/sql中函数相同
  • QueryRow(…) *sql.Row - 跟database/sql中函数相同

同时,sqlxdatabase/sql内置操作做了以下扩展:

  • MustExec() sql.Result – 执行Exec(…),错误时抛出panic异常;
  • Queryx(…) (*sqlx.Rows, error) - 对Query(…)的扩展,返回sqlx.Rows类型结果;
  • QueryRowx(…) *sqlx.Row – 对QueryRow(…)的扩展,返回sqlx.Row类型结果;

sqlx还引入了两个新的操作:

  • Get(dest interface{}, …) error
  • Select(dest interface{}, …) error

我们逐个来讲解。

Exec

ExecMustExec从数据库连接池获取一个连接,然后在服务器上执行对应的sql语句。对于不支持即时查询(ad-hoc query)的数据库驱动,系统会自动新建一个预定义语句(prepared statement)然后执行对应sql操作。结果返回以后立即释放连接到连接池。

schema := `CREATE TABLE place (
    country text,
    city text NULL,
    telcode integer);`
// 直接执行sql操作
result, err := db.Exec(schema)
// 或者,通过MustExec执行,错误时抛出panic异常
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)

返回结果result依赖于不同的驱动,一般包括两部分数据:LastInsertedId()RowsAffected()。例如,在MySQL中,可以通过 LastInsertedId() 在插入操作后直接获取自增以后的关键值;但是在PostgreSQL中,只能通过标准的 RETURNING 语句来获取。

bindvars

检索占位符 “?” 很重要,内部也叫作“bindvars”;你要尽可能得使用这些占位符来发送参数值,因为这样可以 避免SQL注入攻击 。标准库database/sql不对检索sql做任何验证;所有sql语句,包括编码后的参数,都被原封不动得发送到server。除非数据库驱动实现特殊的接口,否则server端会自动准备要执行的sql,然后执行。因此Bindvars是依赖于具体的数据库的:

  • MySQL 使用上边讲的作为占位符;
  • PostgreSQL 使用编号的1,2作为占位符;
  • SQLite 接受?$1两种形式的占位符;
  • Oracle 使用:name的格式;

其它数据库可能还会有一些差异。你都可以在你当前的数据库上通过在 sqlx.DB.Rebind(string) string 函数中使用?占位符来生成需要的sql检索。

一个常见的误区是把binvars用作数值插入。上面讨论的占位符只用来参数化,并不允许修改sql语句的语法结构。比如,用bindvars来尝试参数化列名和表名都是不工作的:

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

Query

Query是执行查询并返回结果row数据的首选方法。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)
}

注意这里应该把返回结果Rows当做 数据库滑标,而不是序列化的列表结构。尽管每个数据库的buffer机制不同,通过Next()来遍历结果可以有效限制大结果集的内存使用,因为每次仅处理单行数据。Sacn()的使用可以用来把sql列类型映射为对应的Go类型,如string, []byte等。

如果不遍历整个rows结果,切记调用rows.Close()来释放连接。

Query返回的error可能是准备SQL语句时的错误,也可能是在server端执行SQL时抛出的错误。虽然database/sql会重试10次获取工作连接,仍然有可能发生返回坏连接的情况。一般来说,错误可能来自于非法的SQL语法、类型不匹配,或者不正确的字段或表名。

大多数情况下,由于row.Scan本身不知道底层驱动是否会重用缓存,row.Scan会直接复制从驱动返回的数据。特殊数据类型sql.RawBytes用来表示驱动返回的实际数据(从起始位置开始)。执行Next()后,sql.RawBytes的值就不再有效了,因为内存可能已经被驱动重写了。

Query使用的连接在整个遍历中都有效,一直到所有的行都通过Next遍历完毕,或者直接调用了rows.Close(),连接就会被释放。可以查看database/sql连接池获取更多关于数据库连接的信息。

sqlx中的扩展Queryx跟sql中Query的行为是一样的,但是返回的结果是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(),它会自动扫码返回结果然后存到对应的结构体字段中。你也可以只用db结果标签来表明哪个列名对应哪个字段,或者使用db.MapperFunc()设置一个默认映射函数。默认的映射规则是直接将结构体中的字段名使用strings.Lower转变为小写后直接用作数据表的列名。更多信息可以查看高级扫描部分

QueryRow

QueryRow用来从服务器获取一行数据。它首先从连接池获取一个数据库连接,然后使用Query执行sql查询,并返回一个Row对象;其中Row对象内部又有自己的Rows对象。

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

跟Query不同的是,QueryRow成功的时候只返回一个Row对象,这样可以很安全地使用链式扫码返回结果。如果查询出错,会返回一个错误error。如果没有数据,返回sql.ErrNoRows。如果扫码本身出错,同样会返回错误error。

返回结果Row里的Rows结构会随着扫描而释放,也就是说,数据库连接在结果被扫描后会马上释放。也意味着sql.RawBytes在这里是不能用的,因为指向的内存可能已经释放了。

同样的,sqlx中的扩展QueryRowx会返回一个sqlx.Row,而不是sql.Row;并且实现了和Rows一样的扫描扩展。

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

Get和Select

GetSelect 是 handel类型扩展, 将查询执行与扫描动作合并,为了解释清除他们,我们讲解下什么是可以扫描的类型。

  • 非结构体(struct)类型的 ,比如 string, int 基础类型等,可扫描
  • 实现了 sql.Scanner 接口的,
  • 没有导出字段的结构,例如 time.Time ,可扫描

Get and Select use rows.Scan on scannable types and rows.StructScan on non-scannable types. They are roughly analagous to QueryRow and Query, where Get is useful for fetching a single result and scanning it, and Select is useful for fetching a slice of results:

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 and Select both will close the Rows they create during query execution, and will return any error encountered at any step of the process. Since they use StructScan internally, the details in the advanced scanning section also apply to Get and Select.

Select can save you a lot of typing, but beware! It's semantically different from Queryx, since it will load the entire result set into memory at once. If that set is not bounded by your query to some reasonable size, it might be best to use the classic Queryx/StructScan iteration instead.

事务操作Transaction

参考