现在,我们准备看看如何修改数据和处理事务。如果您习惯于使用 「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
答案是 no。它们 不会 做同样的事情,也请您永远不要这样使用 Query()
。Query()
将返回一个 sql.Rows
,它将保留数据库连接,直到 sql.Rows
关闭。由于可能存在未读数据 (例如,更多的数据行),因此无法使用该连接。在上面的示例中,连接将 永远 不会被释放。垃圾收集器最终将为您关闭底层 net.Conn
,但这可能需要很长时间。此外,database/sql
包会在连接池中继续跟踪连接,希望您在某个时候释放它,以便可以再次使用该连接。因此,此反模式是耗尽资源 (例如,连接过多) 的好方法。
使用事务
在 Go 中,事务本质上是一个保留与数据存储区连接的对象。它可以让您执行到目前为止所看到的所有操作,但可以保证它们将在同一连接上执行。
您可以通过调用 db.Begin()
开启事务,然后使用该函数生成的 Tx
对象上的 Commit()
或 Rollback()
方法来结束事务。在后台,Tx
从池中获得连接,并将其保留,以仅用于该事务。Tx
上的方法一对一映射到您可以在数据库本身上调用的方法,例如 Query()
等。
在事务中创建的预处理语句专门绑定到该事务。有关更多信息,请参见 预处理语句。
您不应该在 SQL 代码中混合使用与事务相关的函数(例如 Begin()
和 Commit()
)和 SQL 关键字(例如 BEGIN
和 COMMIT
)。这可能会导致不良后果:
Tx
数据库对象可能保持打开状态,保留池中的连接而不返回它。- 数据库的状态可能与代表它的 Go 变量的状态不同步。
- 您可能会认为您正在事务内部的单个连接上执行查询,而实际上 Go 已经为您创建了多个不可见的连接,并且某些语句不是该事务的一部分。
在事务内部进行操作时,应注意不要调用 db
变量。进行所有对您使用 db.Begin()
创建的 Tx
变量的调用。db
不在事务中,只有 Tx
对象在事务中。如果您进一步调用 db.Exec()
或类似方法,则这些调用将在事务范围之外发生在其他连接上。
如果您需要使用多个修改连接状态的语句,即使您本身不需要事务,也需要 Tx
。例如:
- 创建临时表,仅对一个连接可见。
- 设置变量,例如 MySQL 的
SET @var:= somevalue
语法。 - 更改连接选项,例如字符集或超时。
如果您需要执行上述任何操作,则需要将活动绑定到单个连接,而 Go 中唯一的方法是使用 Tx
。
转自:https://learnku.com/docs/go-database-sql/9478.md