本文翻译自《Executing transactions》。
目录
你可以使用代表事务的sql.Tx
结构体执行数据库事务操作。除了表示事务特定语义的Commit
和Rollback
方法之外,sql.Tx
还具有用来执行常见数据库操作的所有方法。要获取sql.Tx
,你可以调用DB.Begin
或DB.BeginTx
方法。
数据库事务将多个数据库操作合成一组,作为更大目标的一部分。其中的所有数据库操作都必须成功执行或都不执行,在任何一种情况下都会保持数据的完整性。 通常,事务操作的流程包括:
1 开始事务。
2 执行一组数据库操作。
3 如果没有错误发生,提交事务以进行数据库更改。
4 如果发生错误,回滚事务以保持数据库不变。
sql
包提供了开始和结束事务的方法,以及执行中间数据库操作的方法。这些方法对应于上述流程中的四个步骤。
1 开始事务。
DB.Begin
或DB.BeginTx
开始一个新的数据库事务,返回一个代表它的sql.Tx
结构体。
2 执行数据库操作。
使用sql.Tx
,你可以在单个数据库连接中查询或更新数据库。为了支持这一点,Tx
导出了以下方法:
Exec
和ExecContext
通过SQL语句(例如如INSERT
、UPDATE
和DELETE
)更改数据库。有关详细信息,请参阅执行不返回行数据的SQL语句。Query
、QueryContext
、QueryRow
和QueryRowContext
用于执行返回行数据的SQL语句。有关更多信息,请参阅查询数据。Prepare
、PrepareContext
、Stmt
和StmtContex
t用于预定义准备好的语句。有关更多信息,请参阅使用准备好的语句。
3 使用以下其中一项结束事务:
- 使用
Tx.Commit
方法提交事务。
如果Commit
方法执行成功(返回nil
错误),则所有查询结果都被确认为有效,并且所有已执行的更新都作为单个原子更改应用于数据库。如果Commit
方法执行失败,则Tx
上Query
和Exec
的所有结果都应被视为无效而丢弃。
- 使用
Tx.Rollback
方法回滚事务。
即使Tx.Rollback
方法执行失败,事务也不再有效,也不会提交到数据库。
最佳实践
遵循以下最佳实践,以更好地应对事务有时需要的复杂语义和连接管理。
- 使用本节中描述的API来管理事务。不要直接使用
BEGIN
和COMMIT
等与事务相关的SQL语句——这样做会使你的数据库处于不可预测的状态,尤其是在并发程序中。 - 使用事务的过程中,请注意不要直接调用非事务的
sql.DB
的方法,因为它们会在事务外部执行,从而使你的代码对数据库状态的看法不一致,甚至会导致死锁。
例子
以下示例中的函数使用事务为相册(album)创建新的客户订单(customer order)。在此过程中,该函数的代码将:
1 开始事务。
2 延迟事务的回滚。如果事务执行成功,它将在该函数退出之前提交,使延迟回滚调用(defer tx.Rollback()
)成为空操作。如果事务执行失败,则不会提交,这意味着回滚将在函数退出时被调用。
3 确认客户订购的专辑有足够的库存(inventory)。
4 如果足够,更新库存数量,减少订购的专辑数目。
5 创建一个新订单并获取新订单为客户生成的ID。
6 提交事务并返回这个ID。
此示例的Tx
方法需要一个context.Context
参数。使得函数的执行(包括数据库操作)在运行时间过长或客户端连接关闭时被取消。有关更多信息,请参阅取消正在进行中的数据库操作。
// CreateOrder函数为相册(album)创建一个新的客户订单(customer order)并返回这个新订单的ID。
func CreateOrder(ctx context.Context, albumID, quantity, custID int) (orderID int64, err error) {
// 创建一个帮助函数用于返回失败的结果。
fail := func(err error) (int64, error) {
return 0, fmt.Errorf("CreateOrder: %v", err)
}
// 获取一个事务实例tx用来执行事务操作。
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fail(err)
}
// 延迟执行一个回滚操作,以防在此事务中某些操作执行失败
defer tx.Rollback()
// 确认相册库存足以下这个订单。
var enough bool
if err = tx.QueryRowContext(ctx, "SELECT (quantity >= ?) from album where id = ?",
quantity, albumID).Scan(&enough); err != nil {
if err == sql.ErrNoRows {
return fail(fmt.Errorf("no such album"))
}
return fail(err)
}
if !enough {
return fail(fmt.Errorf("not enough inventory"))
}
// 更新相册库存以减去本订单中的相册数目。
_, err = tx.ExecContext(ctx, "UPDATE album SET quantity = quantity - ? WHERE id = ?",
quantity, albumID)
if err != nil {
return fail(err)
}
// 在album_order表中创建一个新行。
result, err := tx.ExecContext(ctx, "INSERT INTO album_order (album_id, cust_id, quantity, date) VALUES (?, ?, ?, ?)",
albumID, custID, quantity, time.Now())
if err != nil {
return fail(err)
}
// 获得刚刚创建的订单条目的ID。
orderID, err = result.LastInsertId()
if err != nil {
return fail(err)
}
// 提交这个事务。
if err = tx.Commit(); err != nil {
return fail(err)
}
// 返回这个订单的ID。
return orderID, nil
}