现在我们已经准备好如何去修改数据和使用事务了.如果你已经习惯了使用一个statement对象来获取或更新数据,那么你会发现,两者在Go中有着明显的区别,其被人为地区分了出来.

修改数据的语句

Exec(),最适合与预编译语句一起使用来完成INSERT,UPDATE, DELETE,以及其他不用返回行的语句.下面的例子将演示如何插入一行并且检查操作返回的元数据.

stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
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未关闭前会一直占用连接. 因为也许存在未读的(多行)数据,所以这个连接会一直被使用.在上面的例子中,这种连接永远**都不会被释放.gc最终会关闭底层的net.Conn,但这会很耗时.此外,database/sql包也会追踪连接池中的这个连接,期待你在某一时刻会释放它从而让这个连接再使用. 因此,这种反模式很容易导致资源失控(比如,太多的连接).

使用事务

在Go中,事务的本质是一个对象,可存储与数据库的连接.它允许你做目前你所接触过的所有操作,但必须在同一个连接中完成.

通过调用db.Begin()来开启一个事务,再通过调用变量TxCommit()Rollback()方法来关闭事务.在其内部,Tx从连接池中获取一个连接,存起来以备这个事务使用.Tx的方法和你调用数据库时的方法是一一对应的,比如Query()等等.

在事务中创建的预编译语句也是绑定在这个事务上的.(不能在事务外使用,同样的,在数据库句柄上创建的(即事务外创建的)预编译语句也不能在事务中使用).可在预编译语句中了解更多内容.

请不要将事务的Begin()Commit()函数与SQL语句中的BEGINCOMMIT混淆.这会引发下面中的坏结果:

  • Tx对象会一直保持打开状态,而不会将连接释放回连接池.
  • 数据库的状态不再和代码中与其对应的变量的状态同步.
  • 你会认为你是在事务内部的连接上执行查询,实际上Go为你隐式地创建了多个连接,而且这些语句并不属于这个事务.

在事务内操作时,你应当小心,不要使用db变量,而是全部使用你调用db.Begin()时创建的Tx变量.db不属于事务,Tx对象才是.如果你再调用db.Exec()或类似的,这些操作会发生在事务外的其他连接上.

如果你需要使用多种语句来修改连接的状态,那么你就需要Tx,即使你自身不想使用事务.例如:

  • 创建临时表,这些临时表只对一个连接可见.
  • 设置变量,比如MySQL的语法SET @var := somevalue.
  • 修改连接的参数,比如字符集和超时时间.

如果你想做这些事,那么你就必须在单一连接中进行,而在Go中唯一的做法就是使用事务.