事务 (Transactions)
启动事务
// GenTx 在事务中生成实体组。
func GenTx(ctx context.Context, client *ent.Client) error {
tx, err := client.Tx(ctx)
if err != nil {
return fmt.Errorf("启动事务失败: %w", err)
}
hub, err := tx.Group.
Create().
SetName("Github").
Save(ctx)
if err != nil {
return rollback(tx, fmt.Errorf("创建群组失败: %w", err))
}
// 创建群组管理员。
dan, err := tx.User.
Create().
SetAge(29).
SetName("Dan").
AddManage(hub).
Save(ctx)
if err != nil {
return rollback(tx, err)
}
// 创建用户 "Ariel"。
a8m, err := tx.User.
Create().
SetAge(30).
SetName("Ariel").
AddGroups(hub).
AddFriends(dan).
Save(ctx)
if err != nil {
return rollback(tx, err)
}
fmt.Println(a8m)
// 输出:
// User(id=2, age=30, name=Ariel)
// 提交事务。
return tx.Commit()
}
// rollback 调用 tx.Rollback 并在发生回滚错误时
// 将给定错误与回滚错误打包返回。
func rollback(tx *ent.Tx, err error) error {
if rerr := tx.Rollback(); rerr != nil {
err = fmt.Errorf("%w: %v", err, rerr)
}
return err
}
如果在事务成功后查询已创建实体的边(例如 a8m.QueryGroups()),必须调用 Unwrap() 方法。Unwrap 会将实体内部嵌入的基础客户端状态恢复为非事务版本。
注意
在非事务实体上调用 Unwrap()(即事务已提交或回滚后)
会导致 panic。
完整示例请参见 GitHub。
事务客户端
有时,已有代码已在使用 *ent.Client,而你需要修改(或包装)它以支持事务操作。针对这些场景,可以使用事务客户端——一个可以从现有事务中获取的 *ent.Client。
// WrapGen 将现有的 "Gen" 函数包装在事务中。
func WrapGen(ctx context.Context, client *ent.Client) error {
tx, err := client.Tx(ctx)
if err != nil {
return err
}
txClient := tx.Client()
// 使用下面的 "Gen" 函数,但传入事务客户端;无需修改 "Gen" 的代码。
if err := Gen(ctx, txClient); err != nil {
return rollback(tx, err)
}
return tx.Commit()
}
// Gen 生成一组实体。
func Gen(ctx context.Context, client *ent.Client) error {
// ...
return nil
}
完整示例请参见 GitHub。
最佳实践
可复用的事务回调函数:
func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error {
tx, err := client.Tx(ctx)
if err != nil {
return err
}
defer func() {
if v := recover(); v != nil {
tx.Rollback()
panic(v)
}
}()
if err := fn(tx); err != nil {
if rerr := tx.Rollback(); rerr != nil {
err = fmt.Errorf("%w: 回滚事务失败: %v", err, rerr)
}
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("提交事务失败: %w", err)
}
return nil
}
使用示例:
func Do(ctx context.Context, client *ent.Client) {
// 使用 WithTx 辅助函数。
if err := WithTx(ctx, client, func(tx *ent.Tx) error {
return Gen(ctx, tx.Client())
}); err != nil {
log.Fatal(err)
}
}
钩子
与 schema hooks 和 runtime hooks 类似,可以在活动事务上注册钩子,这些钩子会在 Tx.Commit 或 Tx.Rollback 时执行:
func Do(ctx context.Context, client *ent.Client) error {
tx, err := client.Tx(ctx)
if err != nil {
return err
}
// 在 Tx.Commit 上添加钩子。
tx.OnCommit(func(next ent.Committer) ent.Committer {
return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
// 实际提交前的代码。
err := next.Commit(ctx, tx)
// 事务提交后的代码。
return err
})
})
// 在 Tx.Rollback 上添加钩子。
tx.OnRollback(func(next ent.Rollbacker) ent.Rollbacker {
return ent.RollbackFunc(func(ctx context.Context, tx *ent.Tx) error {
// 实际回滚前的代码。
err := next.Rollback(ctx, tx)
// 事务回滚后的代码。
return err
})
})
//
// <业务代码写在这里>
//
return err
}
隔离级别
某些驱动支持调整事务的隔离级别。例如,使用 sql 驱动时,可以通过 BeginTx 方法实现:
tx, err := client.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})