修改数据和使用事务
现在我们已经准备好了解如何修改数据和处理事务. 如果你习惯于编写使用语句
对象来获取行以及更新数据的语言,那么区别似乎是假的,但在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)
执行 Exec()
后返回了一个 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()
等.
在事务中创建的预编译语句专门绑定到该事务. 详见 使用预编译语句.
别把事务特有的函数 Begin()
, Commit()
与 SQL 语句中的 BEGIN
, COMMIT
混淆了, 否则会有不好的事情发生:
- Tx 对象始终保有一个与数据库之间的连接并且不会被其他对象使用.
- 数据库状态可能与表示它的 Go 变量数据不同步.
- 你可能会觉得你在单一连接上执行查询, 在一个事务内部, 实际上 Go 会创建好几个你不可见的连接, 并且某些语句不是事务的一部分.
当你处于事务内部时, 不要对 db 变量发起调用. 你需要使用通过 db.Begin() 产生的 Tx 变量来发起所有调用. db 不在事务内, 只有 Tx 对象是. 如果你对 db.Exec() 发起进一步地调用, 那么这些调用将会发生在事务外部或其他连接上.
如果你需要使用多个修改连接状态的语句, 即使你不想要事务本身, 你还是需要使用 Tx, 比如:
- 创建只对一个连接可见的临时表.
- 设置变量, 如 MySQL 的 SET 语法, @var := somevalue.
- 更改连接选项, 如字符集或超时时间.
做以上任何一个操作, 你都需要将你的操作绑定到单个连接上, 在 Go 中能干这事的唯一方法就是使用事务对象 Tx.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论