ORM 事务:g.DB().Transaction 用法与错误回滚
引言
转账要"扣款 + 入账"同时成功或同时失败,这就是事务。gdb 提供 g.DB().Transaction(ctx, fn) 的事务封装:回调里出错自动回滚,全部成功自动提交。
概念
- 事务入口:
g.DB().Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {...})。 - 事务内操作:用回调参数
tx的 Model 执行读写(tx.Model("表")...),保证 SQL 走同一事务连接。 - 提交/回滚规则:回调返回
nil则提交;返回非 nil 错误则回滚,并把错误原样返回给调用方。 - 嵌套事务:框架支持 SavePoint 方式的嵌套事务,细节以官方文档 gdb 事务章节为准。
代码示例
经典场景:给订单加一条记录,同时扣减库存,两步必须同生共死。
package main
import (
"context"
"fmt"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
func main() {
ctx := gctx.New()
err := g.DB().Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
// 1. 写订单表
result, err := tx.Model("order").Ctx(ctx).
Data(g.Map{
"user_id": 1,
"goods_id": 100,
"amount": 88.5,
}).
Insert()
if err != nil {
return err // 出错即回滚
}
orderId, _ := result.LastInsertId()
fmt.Println("order id =", orderId)
// 2. 扣减库存(注意 where 中带上数量条件做乐观校验)
result, err = tx.Model("goods").Ctx(ctx).
Data(g.Map{"stock": gdb.Raw("stock - 1")}).
Where("id", 100).
Where("stock > ?", 0).
Update()
if err != nil {
return err // 出错即回滚
}
affected, _ := result.RowsAffected()
if affected == 0 {
// 库存不足:返回错误触发整体回滚
return gerror.New("库存不足")
}
return nil // 全部成功,提交
})
if err != nil {
fmt.Println("事务失败,已回滚:", err)
return
}
fmt.Println("事务提交成功")
}
其中 gdb.Raw("stock - 1") 表示把该值原样作为 SQL 片段,而不是当作字符串参数,适合列自增/自减场景。
事务中查询也要走 tx,否则查不到未提交的数据(隔离级别内的一致性):
err := g.DB().Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
one, err := tx.Model("order").Ctx(ctx).Where("user_id", 1).One()
if err != nil {
return err
}
_ = one
return nil
})
注意点
- 回调里只允许用
tx派生的 Model 操作本事务;用全局g.DB().Model(...)会脱离事务。 - 事务内尽量短:别在回调里做慢查询、外部 HTTP 调用,连接会被长时间占用。
- 库存/余额这类场景建议加条件更新(
Where("stock > ?", 0))做并发保护,回滚救不了逻辑 bug。 - 事务方法返回的错误自带上下文,可在外层用 gerror 再包装一层说明。
小结
Transaction(ctx, fn) 用"回调返回 nil 提交、返回 error 回滚"的约定,把事务复杂度收敛在一处,配合 tx.Model 保证操作归属同一连接。嵌套事务、隔离级别等高级话题以官方文档 goframe.org 的 ORM 事务章节为准。