要在Go中访问数据库,你可以使用sql.DB。你使用这种类型来创建语句和事务,执行查询,并获取结果。
你应该知道的第一件事是,sql.DB不是一个数据库连接。它也没有映射到任何特定数据库软件的 "database"或 "schema"的概念。它是对已存在的数据库的接口抽象,已存在的数据库可能是一个本地文件,通过网络连接访问,或在内存和进程中。
sql.DB在幕后为你执行一些重要的任务。
它通过驱动程序打开和关闭与实际底层数据库的连接。
它根据需要管理一个连接池,如前所述,它可能是各种不同的东西。
sql.DB抽象的设计是为了让你不用担心如何管理对底层数据存储的并发访问。当你用它来执行一个任务时,一个连接被标记为使用中,当它不再被使用时,就返回到可用池中。这样做的一个后果是,如果你没有把连接释放回池中,你会导致sql.DB打开大量的连接,可能会耗尽资源(太多的连接,太多的打开的文件句柄,缺乏可用的网络端口,等等)。我们将在后面讨论更多关于这个问题。
在创建一个sql.DB之后,你可以用它来查询它所代表的数据库,以及创建语句和事务。
要使用database/sql,你需要软件包本身,以及你要使用的特定数据库的驱动。
你一般不应该直接使用驱动包,尽管有些驱动鼓励你这样做。(在我们看来,这通常是个坏主意。)相反,如果可能的话,你的代码应该只引用database/sql中定义的类型。这有助于避免使你的代码依赖于驱动,这样你就可以用最小的代码改动来改变底层驱动(以及你所访问的数据库)。这也迫使你使用Go的习性,而不是某个特定驱动作者可能提供的临时习性。
在本文档中,我们将使用github.com/sijms/go-ora/v2驱动作为例子。
在你的Go源文件的顶部添加以下内容。
import (
"database/sql"
_ "github.com/sijms/go-ora/v2"
)
请注意,我们是以匿名方式加载驱动程序,将其软件包限定符别名为_,这样我们的代码就看不到其导出的名称了。在引擎盖下,驱动程序将自己注册为可用于database/sql包,但一般来说,除了运行init函数外,没有其他事情发生。
现在你已经准备好访问一个数据库了。
现在你已经加载了驱动包,你准备创建一个数据库对象,一个sql.DB。
为了创建一个sql.DB,你使用sql.Open()。这将返回一个*sql.DB。
func main() {
db, err := sql.Open("oracle",
"oracle://user:password@localhost:1521/pdb1")
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
在所示的例子中,我们要说明几件事。
err = db.Ping()
if err != nil {
// do something here
}
尽管当你完成了对数据库的操作时,Close()是一种习惯,但sql.DB对象是被设计成长期存在的。不要经常Open()和Close()数据库。相反,为你需要访问的每个不同的数据存储创建一个sql.DB对象,并保留它直到程序完成对该数据存储的访问。根据需要传递它,或者以某种方式让它在全局范围内可用,但要保持它的开放。不要从一个短命的函数中Open()和Close()。相反,将sql.DB作为一个参数传递给那个短命的函数。
如果你不把sql.DB当作一个长寿命的对象,你可能会遇到一些问题,比如连接的重复使用和共享不好,可用的网络资源耗尽,或者由于大量的TCP连接停留在TIME_WAIT状态而出现零星的故障。这样的问题是你没有按照设计要求使用数据库/sql的标志。
现在是时候使用你的sql.DB对象了。
有几个习惯性的操作可以从数据存储中检索结果。
让我们看一个如何查询数据库的例子,处理结果。我们将查询用户表中一个id为1的用户,并打印出该用户的id和名字。我们将用rows.Scan()把结果分配给变量,一次分配一行。
var (
id int
name string
)
rows, err := db.Query("select id, name from users where id = :1", 1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
log.Println(id, name)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
以下是上述代码中发生的情况。
这其中有几个部分很容易出错,而且会产生不好的后果。
当您迭代行并将其扫描到目标变量时,Go在幕后为您执行数据类型转换工作。它是基于目标变量的类型。意识到这一点可以清理你的代码并帮助避免重复工作。
例如,假设你从一个定义有字符串列的表中选择一些行,比如VARCHAR(45)或类似的。然而,你碰巧知道,这个表总是包含数字。如果你传递一个指向字符串的指针,Go将把字节复制到字符串中。现在你可以使用strconv.ParseInt()或类似的方法将该值转换为数字。你必须检查SQL操作的错误,以及解析整数的错误。这是很混乱和乏味的。
或者,你可以直接给Scan()传递一个指向整数的指针。Go会检测到这一点并为你调用strconv.ParseInt()。如果在转换过程中出现了错误,调用Scan()就会返回。你的代码现在变得更整洁、更小了。这是使用database/sql的推荐方式。
一般来说,你应该总是准备要多次使用的查询。准备查询的结果是一个prepared statement,它可以有占位符(也就是绑定值),用于执行语句时提供的参数。这比连接字符串要好得多,因为所有通常的原因(例如,避免SQL注入攻击)。
在Oracle中,占位符以冒号开始并被命名,如:param1。我们将使用:param1因为我们使用Oracle作为我们的例子。
stmt, err := db.Prepare("select id, name from users where id = :1")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query(1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
// ...
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
在引擎盖下,db.Query()实际上是准备、执行和关闭一个prepared statement。这是对数据库的三次往返。如果你不小心,你可能会使你的应用程序的数据库交互次数增加三倍!一些驱动程序可以在特定情况下避免这种情况,但不是所有的驱动程序都能做到。
如果一个查询最多只返回一条记录,你可以使用一个快捷方式来解决一些冗长的模板代码。
var name string
err = db.QueryRow("select name from users where id = :1", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
来自查询的错误被推迟到Scan()被调用,然后从那里返回。你也可以在一个prepared statement上调用QueryRow()。
stmt, err := db.Prepare("select name from users where id = :1")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
var name string
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
现在我们准备看看如何修改数据和使用事务。如果你习惯于使用 "statement"对象来获取行和更新数据的编程语言,那么这种区别可能显得很不自然,但在Go中,这种区别有一个重要的原因。
使用Exec(),最好是使用准备好的语句,来完成INSERT、UPDATE、DELETE或其他不返回行的语句。下面的例子显示了如何插入一条记录并检查关于该操作的元数据。
stmt, err := db.Prepare("INSERT INTO users(name) VALUES(:1)")
if err != nil {
log.Fatal(err)
}
res, err := stmt.Exec("Dolly")
if err != nil {
log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
log.Fatal(err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
log.Fatal(err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)
执行该语句会产生一个sql.Result,可以访问语句元数据:最后插入的ID和受影响的行数。
如果你不关心这个结果呢?如果你只是想执行一个语句,检查是否有任何错误,但忽略结果呢?下面两个语句可以做同样的事情吗?
_, err := db.Exec("DELETE FROM users") // OK
_, err := db.Query("DELETE FROM users") // BAD
答案是否定的。它们做的不是同一件事,你不应该像这样使用Query()。Query()将返回一个sql.Rows,它保留一个数据库连接,直到sql.Rows被关闭。由于可能有未读的数据(如更多的数据行),所以不能使用该连接。在上面的例子中,该连接将永远不会被释放。垃圾收集器最终将为你关闭底层的net.Conn,但这可能需要很长的时间。此外,database/sql包在它的池子里一直在跟踪这个连接,希望你在某个时候释放它,这样这个连接就可以被再次使用。因此,这种反模式是耗尽资源的一个好方法(例如,太多的连接)。
在Go中,事务本质上是一个保留与数据存储连接的对象。它可以让你进行我们到目前为止所看到的所有操作,但保证这些操作将在同一个连接上执行。
你可以通过调用db.Begin()来开始一个事务,并通过对产生的Tx变量的Commit()或Rollback()方法来关闭它。Tx从池中获得一个连接,并保留它只用于该事务。Tx上的方法与你可以在数据库上调用的方法一一对应,比如Query()等等。
在一个事务中创建的Prepared statements只与该事务绑定。更多信息请参见Prepared statements。
你不应该在你的SQL代码中混合使用事务相关的函数,如Begin()和Commit()与SQL语句,如BEGIN和COMMIT。可能会导致不好的事情。
当你在一个事务内工作时,你应该注意不要调用db变量。所有的调用都是对你用db.Begin()创建的Tx变量进行的。db不在事务中,只有Tx对象在。如果你进一步调用db.Exec()或类似的语句,这些将发生在你的事务范围之外,在其他连接上。
如果你需要处理多个修改连接状态的语句,你需要一个Tx,即使你不想要一个事务本身。比如说。
Prepared Statements在Go中具有所有常见的优点:安全、高效、方便。但是它们的实现方式与你可能习惯的方式有些不同,特别是在它们与database/sql的一些内部交互方面。
在数据库层面上,一个准备好的语句被绑定到一个数据库连接上。典型的流程是,客户端向服务器发送一个带有占位符的SQL语句进行准备,服务器用一个语句ID进行响应,然后客户端通过发送其ID和参数来执行该语句。
然而,在Go中,连接并不直接暴露给database/sql包的用户。你不会在连接上准备一个语句。你是在DB或Tx上准备它。而且database/sql有一些方便的行为,比如自动重试。由于这些原因,准备好的语句和连接之间的底层关联,存在于驱动层,对你的代码是隐藏的。
以下是它是如何工作的。
因为当语句的原始连接繁忙时,语句会根据需要被重新准备,数据库的高并发使用,可能会使很多连接保持繁忙,从而产生大量的准备好的语句。这可能会导致明显的语句泄漏,语句的准备和重新准备比你想象的更频繁,甚至会遇到服务器端对语句数量的限制。
Go为你创建了Prepared Statements。例如,一个简单的db.Query(sql, param1, param2),通过准备sql,然后用参数执行它,最后关闭语句。
然而,有时Prepared Statements并不是你想要的。这可能有几个原因。
在Tx中创建的Prepared Statements只与该Tx绑定,所以前面关于重新准备的警告并不适用。当你对一个Tx对象进行操作时,你的操作直接映射到它下面的一个也是唯一的一个连接。
这也意味着在Tx内部创建的Prepared Statements不能与它分开使用。同样,在DB上创建的Prepared Statements也不能在事务中使用,因为它们将被绑定到一个不同的连接。
要在Tx中使用在事务外Prepared Statements,你可以使用Tx.Stmt(),它将从在事务外Prepared Statements中创建一个新的事务专用语句。它的做法是,在现有的Prepared Statements中,将连接设置为事务的连接,并在每次执行时重新准备所有的语句。这种行为和它的实现是不可取的,在database/sql源代码中甚至有一个TODO来改进它;我们建议不要使用这个。
在事务中使用准备好的语句时,必须谨慎行事。考虑一下下面的例子。
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO foo VALUES (?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close() // danger!
for i := 0; i < 10; i++ {
_, err = stmt.Exec(i)
if err != nil {
log.Fatal(err)
}
}
err = tx.Commit()
if err != nil {
log.Fatal(err)
}
// stmt.Close() runs here!
在Go 1.4之前,关闭*sql.Tx会将与之相关的连接释放到池中,但在prepared statement上对Close的延迟调用是在这之后执行的,这可能导致对底层连接的并发访问,使连接状态不一致。如果你使用Go 1.4或更早的版本,你应该确保语句总是在事务提交或回滚之前被关闭。这个问题在Go 1.4中由CR 131650043修复。
预备语句中的占位参数的语法是特定于数据库的。例如,比较一下MySQL、PostgreSQL和Oracle。
MySQL PostgreSQL Oracle
===== ========== ======
WHERE col = ? WHERE col = $1 WHERE col = :col
VALUES(?, ?, ?) VALUES($1, $2, $3) VALUES(:val1, :val2, :val3)
几乎所有的database/sql类型的操作都会返回一个错误作为最后的值。你应该经常检查这些错误,千万不要忽视它们。
有几个地方的错误行为是特殊情况,或者有一些额外的东西你可能需要知道。
考虑一下下面的代码。
for rows.Next() {
// ...
}
if err = rows.Err(); err != nil {
// handle the error here
}
来自rows.Err()的错误可能是rows.Next()循环中各种错误的结果。循环可能因为某些原因退出,而不是正常完成循环,所以你总是需要检查循环是否正常终止。一个不正常的终止会自动调用rows.Close(),尽管多次调用它是无害的。
如前所述,如果你过早地退出循环,你应该总是显式地关闭一个sql.Rows。如果循环正常退出或出现错误,它将自动关闭,但你可能会错误地这样做。
for rows.Next() {
// ...
break; // whoops, rows is not closed! memory leak...
}
// do the usual "if err = rows.Err()" [omitted here]...
// it's always safe to [re?]close here:
if err = rows.Close(); err != nil {
// but what should we do if there's an error?
log.Println(err)
}
rows.Close()返回的错误是一般规则的唯一例外,即最好在所有数据库操作中捕获和检查错误。如果rows.Close()返回一个错误,不清楚你应该做什么。记录错误信息或惊慌失措可能是唯一明智的做法,如果这是不明智的,那么也许你应该忽略这个错误。
考虑用下面的代码来获取一条记录。
var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
如果没有id = 1的用户呢?那么结果中就没有行,而且.Scan()不会将一个值扫描到name中。那会发生什么呢?
Go定义了一个特殊的错误常量,称为sql.ErrNoRows,当结果为空时,它将从QueryRow()返回。在大多数情况下,这需要作为一种特殊情况来处理。一个空的结果通常不被应用程序代码认为是一个错误,如果你不检查一个错误是否是这个特殊的常量,你会导致你没有想到的应用程序代码错误。
来自查询的错误被推迟到Scan()被调用,然后再从那里返回。上面的代码最好这样写。
var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
// there were no rows, but otherwise no error occurred
} else {
log.Fatal(err)
}
}
fmt.Println(name)
有人可能会问,为什么一个空的结果集被认为是一个错误。空集并没有什么错误。原因是QueryRow()方法需要使用这种特殊情况,以便让调用者区分QueryRow()是否真的找到了一行;如果没有它,Scan()就不会做任何事情,你可能不会意识到你的变量毕竟没有从数据库中得到任何值。
你应该只在使用QueryRow()时遇到这个错误。如果你在其他地方遇到这个错误,你就做错了。
写下面这样的代码是很诱人的。
rows, err := db.Query("SELECT someval FROM sometable")
// err contains:
// ERROR 1045 (28000): Access denied for user 'foo'@'::1' (using password: NO)
if strings.Contains(err.Error(), "Access denied") {
// Handle the permission-denied error
}
但这并不是最好的方法。例如,字符串的值可能会有所不同,这取决于服务器使用什么语言来发送错误信息。比较错误号码以确定具体的错误是什么要好得多。
然而,不同的驱动程序做这个的机制是不同的,因为这不是database/sql本身的一部分。在本教程所关注的MySQL驱动中,你可以写下面的代码。
if driverErr, ok := err.(*mysql.MySQLError); ok { // Now the error number is accessible directly
if driverErr.Number == 1045 {
// Handle the permission-denied error
}
}
同样,这里的MySQLError类型是由这个特定的驱动程序提供的,.Number字段在不同的驱动程序之间可能有所不同。然而,数字的值取自MySQL的错误信息,因此是数据库特定的,而不是驱动程序特定的。
这段代码仍然很难看。与1045这个神奇的数字相比较,是一种代码的味道。一些驱动(尽管不是MySQL的驱动,因为这里的原因偏离了主题)提供了一个错误标识符的列表。例如,Postgres的pq驱动在error.go中就有。还有一个由VividCortex维护的MySQL错误编号的外部包。使用这样一个列表,上面的代码最好这样写。
if driverErr, ok := err.(*mysql.MySQLError); ok {
if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
// Handle the permission-denied error
}
}
如果你与数据库的连接被放弃、被杀死或出现错误怎么办?
当这种情况发生时,你不需要实现任何逻辑来重试失败的语句。作为database/sql的连接池的一部分,处理失败的连接是内置的。如果你执行一个查询或其他语句,而底层连接出现故障,Go将重新打开一个新的连接(或只是从连接池中获取另一个连接)并重试,最多 10 次。
然而,可能会有一些意想不到的后果。某些类型的错误可能会在其他错误条件发生时被重试。这也可能是特定的驱动程序。一个发生在MySQL驱动上的例子是,使用KILL来取消一个不想要的语句(比如一个长期运行的查询),导致该语句被重试多达10次。
可设置空值的列是很烦人的,而且会导致很多丑陋的代码。如果你可以的话,请避免使用它们。如果不能,那么你就需要使用database/sql包中的特殊类型来处理它们,或者定义你自己的。
有一些类型适用于可归零的布尔运算、字符串、整数和浮点数。下面是你如何使用它们。
for rows.Next() {
var s sql.NullString
err := rows.Scan(&s)
// check err
if s.Valid {
// use s.String
} else {
// NULL value
}
}
nullable类型的限制,以及避免nullable列的原因,如果你需要更多的说服力。
没有sql.NullUint64或sql.NullYourFavoriteType。你需要为这个定义你自己的。
Nullability是很棘手的,而且不能保证未来。如果你认为某些东西不会是空的,但你错了,你的程序就会崩溃,也许很少,以至于你在发货前不会发现错误。
Go的一个好处是为每个变量提供了一个有用的默认零值。这并不是nullable事物的工作方式。
如果你需要定义自己的类型来处理NULL,你可以复制sql.NullString的设计来实现。
如果你不能避免在你的数据库中出现NULL值,还有一个大多数数据库系统都支持的工作方法,即COALESCE()。像下面这样的东西可能是你可以使用的,而不用引入无数的sql.Null*类型。
rows, err := db.Query(`
SELECT
name,
COALESCE(other_field, '') as otherField
WHERE id = ?
`, 42)
for rows.Next() {
err := rows.Scan(&name, &otherField)
// ..
// If `other_field` was NULL, `otherField` is now an empty string. This works with other data types as well.
}
Scan()函数要求你准确传递正确数量的目标变量。如果你不知道查询将返回什么呢?
如果你不知道查询将返回多少列,你可以使用Columns()来找到一个列名的列表。你可以检查这个列表的长度,看看有多少列,你可以用正确的值数将一个片断传给Scan()。例如,MySQL的一些分叉为SHOW PROCESSLIST命令返回不同的列,所以你必须对此有所准备,否则你会引起错误。这里是一种方法,还有其他方法。
cols, err := rows.Columns()
if err != nil {
// handle the error
} else {
dest := []interface{}{ // Standard MySQL columns
new(uint64), // id
new(string), // host
new(string), // user
new(string), // db
new(string), // command
new(uint32), // time
new(string), // state
new(string), // info
}
if len(cols) == 11 {
// Percona Server
} else if len(cols) > 8 {
// Handle this case
}
err = rows.Scan(dest...)
// Work with the values in dest
}
如果你不知道这些列或它们的类型,你应该使用sql.RawBytes。
cols, err := rows.Columns() // Remember to check err afterwards
vals := make([]interface{}, len(cols))
for i, _ := range cols {
vals[i] = new(sql.RawBytes)
}
for rows.Next() {
err = rows.Scan(vals...)
// Now you can check each element of vals for nil-ness,
// and you can use type introspection and type assertions
// to fetch the column into a typed variable.
}
在database/sql中有一个基本的连接池。没有太多的能力来控制或检查它,但这里有一些你可能觉得有用的东西。
虽然database/sql在你习惯使用后很简单,但你可能会对它支持的微妙的使用情况感到惊讶。这在Go的核心库中是很常见的。
正如本网站中提到的,如果你不按预期使用database/sql,你肯定会给自己带来麻烦,通常是消耗一些资源或阻止它们被有效地重复使用。
这里有一个令人惊讶的错误。你不能把大的无符号整数作为参数传递给语句,如果它们的高位被设置。
_, err := db.Exec("INSERT INTO users(id) VALUES", math.MaxUint64) // Error
这将抛出一个错误。如果你使用uint64值,要小心,因为它们开始时可能很小,工作时没有错误,但随着时间的推移,会增加,并开始抛出错误。
有些事情会改变连接状态,这可能会导致问题,原因有二。
例如,用USE语句设置当前数据库是许多人要做的典型事情。但在Go中,它只影响你运行的连接。除非你在一个事务中,否则你认为在该连接上执行的其他语句实际上可能在从池中获取的不同连接上运行,所以它们不会看到这种改变的影响。
此外,在你改变了连接后,它会返回到池中,并有可能对其他代码的状态造成污染。这也是为什么你不应该直接将BEGIN或COMMIT语句作为SQL命令发布的原因之一。
database/sql API提供了一个面向行的数据库的抽象,但是特定的数据库和驱动在行为和/或语法上可能有所不同,比如准备好的语句占位符。
Go驱动不支持单个查询的多个结果集,而且似乎也没有计划这样做,尽管有一个支持批量操作的功能请求,如批量复制。
这意味着,除其他事项外,返回多个结果集的存储过程将无法正常工作。
调用存储过程是特定于驱动程序的,但在MySQL驱动程序中,目前还不能这样做。看起来你可以通过执行这样的程序来调用一个简单的、返回单一结果集的过程。
err := db.QueryRow("CALL mydb.myprocedure").Scan(&result) // Error
事实上,这是不可能的。你会得到下面的错误。错误1312:PROCEDURE mydb.myprocedure不能在给定的上下文中返回一个结果集。这是因为MySQL期望连接被设置为多语句模式,即使是单一的结果,而驱动程序目前并不这样做(尽管见这个问题)。
数据库/sql没有明确的多语句支持,这意味着其行为取决于后端。
_, err := db.Exec("DELETE FROM tbl1; DELETE FROM tbl2") // Error/unpredictable result
服务器可以随意解释,包括返回错误,只执行第一个语句,或者同时执行两个语句。
同样地,在一个事务中也没有办法批量处理语句。事务中的每条语句都必须连续执行,结果中的资源,如一个或多个行,必须被扫描或关闭,以便底层连接可以空出来供下一条语句使用。这与你不使用事务时的通常行为不同。在这种情况下,完全可以执行一个查询,在行上循环,并在循环中对数据库进行查询(这将发生在一个新的连接上)。
rows, err := db.Query("select * from tbl1") // Uses connection 1
for rows.Next() {
err = rows.Scan(&myvariable)
// The following line will NOT use connection 1, which is already in-use
db.Query("select * from tbl2 where id = ?", myvariable)
}
但是事务只与一个连接绑定,所以这在事务中是不可能的。
tx, err := db.Begin()
rows, err := tx.Query("select * from tbl1") // Uses tx's connection
for rows.Next() {
err = rows.Scan(&myvariable)
// ERROR! tx's connection is already busy!
tx.Query("select * from tbl2 where id = ?", myvariable)
}
不过,Go并不能阻止你的尝试。由于这个原因,如果你在第一条语句释放其资源和清理自己之前试图执行另一条语句,你可能会出现连接损坏的情况。这也意味着事务中的每个语句都会导致一组单独的网络往返于数据库。
以下是我们发现的一些有帮助的外部信息来源。
我们希望你能发现这个网站是有帮助的。如果你有任何改进的建议,请发送拉动请求或在https://github.com/VividCortex/go-database-sql-tutorial,打开一个问题报告。
完结!