现在我们已经准备好如何去修改数据和使用事务了.如果你已经习惯了使用一个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()
来开启一个事务,再通过调用变量Tx
的Commit()
和Rollback()
方法来关闭事务.在其内部,Tx
从连接池中获取一个连接,存起来以备这个事务使用.Tx的方法和你调用数据库时的方法是一一对应的,比如Query()
等等.
在事务中创建的预编译语句也是绑定在这个事务上的.(不能在事务外使用,同样的,在数据库句柄上创建的(即事务外创建的)预编译语句也不能在事务中使用).可在预编译语句中了解更多内容.
请不要将事务的Begin()
和Commit()
函数与SQL语句中的BEGIN
和COMMIT
混淆.这会引发下面中的坏结果:
Tx
对象会一直保持打开状态,而不会将连接释放回连接池.- 数据库的状态不再和代码中与其对应的变量的状态同步.
- 你会认为你是在事务内部的连接上执行查询,实际上Go为你隐式地创建了多个连接,而且这些语句并不属于这个事务.
在事务内操作时,你应当小心,不要使用db
变量,而是全部使用你调用db.Begin()
时创建的Tx
变量.db
不属于事务,Tx
对象才是.如果你再调用db.Exec()
或类似的,这些操作会发生在事务外的其他连接上.
如果你需要使用多种语句来修改连接的状态,那么你就需要Tx
,即使你自身不想使用事务.例如:
- 创建临时表,这些临时表只对一个连接可见.
- 设置变量,比如MySQL的语法
SET @var := somevalue
. - 修改连接的参数,比如字符集和超时时间.
如果你想做这些事,那么你就必须在单一连接中进行,而在Go中唯一的做法就是使用事务.