Golang database/sql 学习

15 篇文章 0 订阅

Golang database/sql 学习

Golang database/sql 概述

要在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()
}

在所示的例子中,我们要说明几件事。

  1. sql.Open的第一个参数是驱动程序的名称。这是驱动程序用来向database/sql注册的字符串,习惯上与包的名称相同,以避免混淆。例如,对于github.com/go-sql-driver/mysql,它是mysql。一些驱动程序不遵循惯例,使用数据库名称,例如,github.com/sijms/go-ora/v2为oracle。
  2. 第二个参数是一个特定于驱动的语法,它告诉驱动如何访问底层数据存储。在这个例子中,我们要连接到本地Oracle服务器实例中的 "pdb1"数据库。
  3. 你应该(几乎)总是检查和处理从所有database/sql操作返回的错误。有一些特殊情况,我们将在后面讨论,在这些情况下,这样做是没有意义的。
  4. 如果sql.DB的寿命不应该超过函数的范围,那么defer db.Close()是一种习惯做法。
    也许与直觉相反的是,sql.Open()并没有建立任何与数据库的连接,也没有验证驱动连接参数。相反,它只是为以后的使用准备了数据库的抽象。到底层数据存储的第一个实际连接将在第一次需要时被懒散地建立。如果你想立即检查数据库是否可用和可访问(例如,检查你是否可以建立网络连接并登录),使用db.Ping()来做,并记住检查错误。
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对象了。

检索结果集

有几个习惯性的操作可以从数据存储中检索结果。

  1. 执行一个返回行的查询。
  2. 为重复使用准备一个语句,多次执行它,然后销毁它。
  3. 以一次性的方式执行一个语句,不为重复使用做准备。
  4. 执行一个返回单行的查询。对于这种特殊情况,有一个捷径。
    Go的database/sql函数名是很重要的。如果一个函数名称中包含Query,它被设计为向数据库查询,并将返回一组行,即使它是空的。不返回行的语句不应该使用Query函数;它们应该使用Exec()。

从数据库中获取数据

让我们看一个如何查询数据库的例子,处理结果。我们将查询用户表中一个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)
}

以下是上述代码中发生的情况。

  1. 我们使用db.Query()来发送查询到数据库。我们像往常一样,检查错误。
  2. 我们使用了defer rows.Close()。这一点非常重要。
  3. 我们用rows.Next()遍历这些行。
  4. 我们用rows.Scan()将每行的列读入变量。
  5. 在我们完成对行的迭代后,我们检查错误。
    这几乎是Go中唯一的方法。比如说,你不能把行作为一个map来获取。这是因为所有的东西都是强类型的。你需要创建正确类型的变量并向它们传递指针,如上所示。

这其中有几个部分很容易出错,而且会产生不好的后果。

  • 你应该总是在for rows.Next()循环结束时检查是否有错误。如果在循环过程中出现了错误,你需要知道它。不要只是假设循环迭代直到你处理完所有的行。
  • 第二,只要有一个打开的结果集(由rows代表),底层连接就很忙,不能用于任何其他查询。这意味着它在连接池中是不可用的。如果你用rows.Next()遍历所有的行,最终你会读到最后一行,而rows.Next()会调用一个内部EOF错误,并为你调用rows.Close()。但是如果由于某种原因你退出了这个循环–提前返回,或者其他原因–那么rows不会被关闭,连接仍然是打开的。(如果rows.Next()由于错误而返回false,它就会自动关闭)。这是一个容易耗尽资源的方法。
  • rows.Close()是一个无害的空操作,如果它已经被关闭,所以你可以多次调用它。但是请注意,我们先检查错误,只有在没有错误的情况下才调用rows.Close(),以避免出现运行时panic。
  • 你应该总是defer rows.Close(),即使你在循环的最后明确地调用rows.Close(),这也不是一个坏主意。
  • 不要在一个循环中defer。defer语句在函数退出前不会被执行,所以一个长期运行的函数不应该使用它。如果你这样做,你会慢慢积累内存。如果你在一个循环中反复查询和消耗结果集,你应该在完成每个结果时明确调用rows.Close(),而不是使用defer。

Scan()如何工作

当您迭代行并将其扫描到目标变量时,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。可能会导致不好的事情。

  • Tx对象可能保持打开,从池中保留一个连接而不返回。
  • 数据库的状态可能与代表数据库的Go变量的状态不同步。
  • 你可能认为你是在一个事务内的单个连接上执行查询,而实际上Go已经为你无形中创建了几个连接,而且一些语句并不是事务的一部分。

当你在一个事务内工作时,你应该注意不要调用db变量。所有的调用都是对你用db.Begin()创建的Tx变量进行的。db不在事务中,只有Tx对象在。如果你进一步调用db.Exec()或类似的语句,这些将发生在你的事务范围之外,在其他连接上。

如果你需要处理多个修改连接状态的语句,你需要一个Tx,即使你不想要一个事务本身。比如说。

  • 创建临时表,这只对一个连接可见。
  • 设置变量,如MySQL的SET @var := somevalue语法。
  • 改变连接选项,如字符集或超时。
    如果你需要做这些事情,你需要将你的活动绑定到一个单一的连接上,而在Go中唯一的方法就是使用Tx。

使用Prepared Statements

Prepared Statements在Go中具有所有常见的优点:安全、高效、方便。但是它们的实现方式与你可能习惯的方式有些不同,特别是在它们与database/sql的一些内部交互方面。

Prepared Statements和连接

在数据库层面上,一个准备好的语句被绑定到一个数据库连接上。典型的流程是,客户端向服务器发送一个带有占位符的SQL语句进行准备,服务器用一个语句ID进行响应,然后客户端通过发送其ID和参数来执行该语句。

然而,在Go中,连接并不直接暴露给database/sql包的用户。你不会在连接上准备一个语句。你是在DB或Tx上准备它。而且database/sql有一些方便的行为,比如自动重试。由于这些原因,准备好的语句和连接之间的底层关联,存在于驱动层,对你的代码是隐藏的。

以下是它是如何工作的。

  • 当你准备一个语句时,它是在池中的一个连接上准备的。
  • Stmt对象会记住使用的是哪一个连接。
  • 当你执行Stmt时,它会尝试使用该连接。如果它因关闭或忙于做其他事情而不可用,它会从池中获取另一个连接,并在另一个连接上与数据库re-prepares the statement。

因为当语句的原始连接繁忙时,语句会根据需要被重新准备,数据库的高并发使用,可能会使很多连接保持繁忙,从而产生大量的准备好的语句。这可能会导致明显的语句泄漏,语句的准备和重新准备比你想象的更频繁,甚至会遇到服务器端对语句数量的限制。

避免Prepared Statements

Go为你创建了Prepared Statements。例如,一个简单的db.Query(sql, param1, param2),通过准备sql,然后用参数执行它,最后关闭语句。

然而,有时Prepared Statements并不是你想要的。这可能有几个原因。

  1. 数据库不支持Prepared Statements。例如,当使用MySQL驱动时,你可以连接到MemSQL和Sphinx,因为它们支持MySQL的电线协议。但它们不支持包括准备好的语句的 "二进制 "协议,所以它们会以令人困惑的方式失败。
  2. 语句的重复使用不足以使它们值得,而且安全问题以其他方式处理,所以性能开销是不希望的。这方面的一个例子可以在VividCortex blog中看到。
    如果你不想使用prepared statement,你需要使用fmt.Sprint()或类似的方法来组装SQL,并将其作为唯一的参数传递给db.Query()或db.QueryRow()。你的驱动程序需要支持纯文本查询的执行,这在Go 1.1中通过Execer和Queryer接口添加,这里有记录

事务中的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()返回一个错误,不清楚你应该做什么。记录错误信息或惊慌失措可能是唯一明智的做法,如果这是不明智的,那么也许你应该忽略这个错误。

来自QueryRow()的错误

考虑用下面的代码来获取一条记录。

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次。

使用NULL的工作

可设置空值的列是很烦人的,而且会导致很多丑陋的代码。如果你可以的话,请避免使用它们。如果不能,那么你就需要使用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中有一个基本的连接池。没有太多的能力来控制或检查它,但这里有一些你可能觉得有用的东西。

  • 连接池意味着在一个数据库上执行两个连续的语句可能会打开两个连接并分别执行。对于程序员来说,对他们的代码出现错误的原因感到困惑是相当普遍的。例如,LOCK TABLES之后的INSERT可能会阻塞,因为INSERT是在一个不持有表锁的连接上。
  • 连接是在需要时创建的,而且池中没有空闲的连接。
  • 默认情况下,对连接的数量没有限制。如果你试图一次做很多事情,你可以创建一个任意数量的连接。这可能导致数据库返回一个错误,如 “连接数太多”。
  • 在Go 1.1或更新的版本中,你可以使用db.SetMaxIdleConns(N)来限制池中空闲连接的数量。但这并不限制池的大小。
  • 在Go 1.2.1或更新版本中,你可以使用db.SetMaxOpenConns(N)来限制数据库的总开放连接数。不幸的是,一个死锁错误(修复)使db.SetMaxOpenConns(N)不能在1.2中安全使用。
  • 连接的回收是相当快的。用db.SetMaxIdleConns(N)设置一个高数量的空闲连接可以减少这种流失,并有助于保持连接的重复使用。
  • 长时间保持连接空闲可能会导致问题(比如在微软Azure上的MySQL的这个问题)。如果你因为一个连接闲置时间过长而导致连接超时,可以尝试db.SetMaxIdleConns(0)。
  • 你也可以通过设置db.SetConnMaxLifetime(duration)来指定一个连接可以被重复使用的最大时间,因为重复使用长寿命的连接可能会导致网络问题。这样就可以懒洋洋地关闭未使用的连接,即关闭过期的连接可能被推迟。

惊喜、反模式和局限性

虽然database/sql在你习惯使用后很简单,但你可能会对它支持的微妙的使用情况感到惊讶。这在Go的核心库中是很常见的。

资源耗尽

正如本网站中提到的,如果你不按预期使用database/sql,你肯定会给自己带来麻烦,通常是消耗一些资源或阻止它们被有效地重复使用。

  • 打开和关闭数据库会造成资源的耗尽。
  • 未能读取所有行或使用rows.Close()从池中保留连接。
  • 对一个不返回行的语句使用Query()会从池中保留一个连接。
  • 如果没有意识到准备好的语句是如何工作的,就会导致大量的额外的数据库活动。

大的uint64值

这里有一个令人惊讶的错误。你不能把大的无符号整数作为参数传递给语句,如果它们的高位被设置。

_, err := db.Exec("INSERT INTO users(id) VALUES", math.MaxUint64) // Error

这将抛出一个错误。如果你使用uint64值,要小心,因为它们开始时可能很小,工作时没有错误,但随着时间的推移,会增加,并开始抛出错误。

连接状态不匹配

有些事情会改变连接状态,这可能会导致问题,原因有二。

  1. 一些连接状态,比如你是否在一个事务中,应该通过Go类型来处理。
  2. 你可能假设你的查询是在一个单一的连接上运行,而实际上不是。

例如,用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,打开一个问题报告。

完结!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值