Go服务层事务回滚:entGO仓库用户创建与Token生成的事务处理
基于entGO实现用户创建+Token生成的事务回滚方案
针对你遇到的问题,推荐给仓库层方法增加事务参数(接收*ent.Tx),由服务层控制事务的启动、提交和回滚,这样既能保证原子性,又符合分层设计的职责划分,比事后删除用户或自定义仓库事务函数更可靠。
核心思路
你的痛点在于:原流程是先创建用户(已持久化到数据库),再生成Token,失败后只能删除用户,这会带来数据不一致风险。正确的做法是把用户创建放在未提交的事务里,只有当Token生成成功后再提交事务,失败则直接回滚,从根源避免脏数据。
具体实现步骤
1. 修改仓库层的Create方法,支持事务参数
让Create方法可以接收*ent.Tx参数,内部根据是否传入事务选择对应的客户端(事务客户端或普通客户端),这样同一个方法既支持事务场景,也支持非事务场景:
// mariaUserRepository is data/repository implementation // of service layer UserRepository type mariaUserRepository struct { Client *ent.Client } // NewUserRepository is a factory for initializing User Repositories func NewUserRepository(client *ent.Client) models.UserRepository { return &mariaUserRepository{ Client: client, } } // Create 支持传入事务参数,实现事务内/非事务内的用户创建 func (r *mariaUserRepository) Create(ctx context.Context, u *ent.User, tx *ent.Tx) (*ent.User, error) { // 确定使用的数据库客户端:有事务则用事务客户端,否则用仓库默认客户端 client := r.Client if tx != nil { client = tx.Client() } // 检查用户是否已存在 check, err := client.User. Query(). Where(user.Email(u.Email)).Exist(ctx) if err != nil { utils.Logger.Info("Error when checking user", zap.String("email", u.Email), zap.Error(err), ) return nil, handle_errors.NewInternal() } if check { err := handle_errors.NewConflict("email", u.Email) utils.Logger.Info("Error when Register User", zap.String("email", u.Email), zap.Error(err), ) return nil, err } // 创建用户 createdUser, err := client.User. Create(). SetFirstName(u.FirstName). SetLastName(u.LastName). SetEmail(u.Email). SetGender(u.Gender). SetUserType(u.UserType). SetPasswordHash(u.PasswordHash). Save(ctx) if err != nil { utils.Logger.Info("Error when create user", zap.Any("User", u), zap.Error(err), ) return nil, handle_errors.NewInternal() } return createdUser, nil }
2. 服务层控制事务生命周期
在服务层的Signup方法中启动事务,先在事务内创建用户,再生成Token:
- Token生成成功:提交事务,用户正式持久化
- Token生成失败:回滚事务,用户不会被保存
func (s *userService) Signup(ctx context.Context, u *ent.User) (*ent.User, error) { // 启动ent事务 tx, err := s.userRepo.Client.Tx(ctx) if err != nil { utils.Logger.Info("Failed to start transaction", zap.Error(err)) return nil, handle_errors.NewInternal() } // 处理panic场景,确保事务回滚 defer func() { if p := recover(); p != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { utils.Logger.Info("Failed to rollback transaction on panic", zap.Error(rollbackErr)) } panic(p) } }() // 事务内创建用户(此时用户未真正写入数据库) createdUser, err := s.userRepo.Create(ctx, u, tx) if err != nil { // 创建失败,回滚事务 if rollbackErr := tx.Rollback(); rollbackErr != nil { utils.Logger.Info("Failed to rollback transaction after create user error", zap.Error(rollbackErr)) } return nil, err } // 生成Token _, err = s.tokenService.NewPairFromUser(ctx, createdUser, "") if err != nil { // Token生成失败,回滚事务,用户不会被保存 if rollbackErr := tx.Rollback(); rollbackErr != nil { utils.Logger.Info("Failed to rollback transaction after token generation error", zap.Error(rollbackErr)) } utils.Logger.Info("Failed to create tokens for user", zap.String("userID", createdUser.ID.String()), zap.Error(err), ) return nil, err } // 所有步骤成功,提交事务 if err := tx.Commit(); err != nil { utils.Logger.Info("Failed to commit transaction", zap.Error(err)) return nil, handle_errors.NewInternal() } return createdUser, nil }
为什么不选自定义事务函数?
如果在仓库层封装事务逻辑(比如写一个CreateWithTransaction函数),会把事务边界和数据操作绑定,导致服务层无法将Token生成这类业务逻辑纳入事务判断。仓库层只负责数据访问,事务边界应该由服务层根据业务逻辑决定,这样更灵活,也符合单一职责原则。
内容的提问来源于stack exchange,提问作者naofel1
相关产品推荐
相关产品推荐

