GORM 事务管理与 Repository 模式完整指南

0 阅读7分钟

GORM 事务管理与 Repository 模式完整指南

一、GORM 闭包事务详解

1.1 什么是闭包事务

GORM 的闭包事务(Closure-based Transaction)又称回调事务模式,是通过闭包函数封装事务逻辑的一种方式:

err := db.Transaction(func(tx *gorm.DB) error {
    // 事务内的所有操作
    if err := tx.Create(&user).Error; err != nil {
        return err  // 自动回滚
    }
    if err := tx.Create(&profile).Error; err != nil {
        return err  // 自动回滚
    }
    return nil     // 自动提交
})

1.2 闭包事务的优点

优点说明
代码简洁减少样板代码,自动管理事务边界
自动回滚返回错误时自动回滚,避免连接泄漏
自动提交成功执行后自动提交
事务传播正确确保闭包内使用同一事务对象
错误处理统一统一的事务错误处理机制

1.3 闭包事务的缺点

缺点说明
灵活性受限无法在闭包外控制提交/回滚时机
嵌套事务复杂嵌套事务使用保存点,逻辑复杂
调试困难错误发生点不明确,调试不便
上下文传递不便需要额外处理 context 传递
不支持手动保存点无法在闭包内设置回滚点

1.4 使用场景建议

适合场景:

  • 简单的增删改查组合
  • 快速原型开发
  • 单个服务的本地事务
  • 需要强制事务保证一致性的场景

不适合场景:

  • 需要精细控制提交时机的长事务
  • 分布式事务协调
  • 需要中间检查点的复杂业务
  • 根据中间结果动态调整的事务

二、在事务中调用 Model 方法

2.1 参数传递法(推荐)

将事务对象作为参数传递给 Model 方法:

// Model 方法定义
func (u *User) Create(tx *gorm.DB) error {
    return tx.Create(u).Error
}

// 业务层调用
func CreateUserWithProfile(db *gorm.DB) error {
    return db.Transaction(func(tx *gorm.DB) error {
        user := &User{Name: "张三"}
        if err := user.Create(tx); err != nil {
            return err
        }
        
        profile := &Profile{UserID: user.ID}
        return tx.Create(profile).Error
    })
}

2.2 Repository 模式(企业级推荐)

2.2.1 核心概念

Repository 模式是 DDD 中的数据访问抽象层,核心价值:

  • 分离业务逻辑和数据访问逻辑
  • 提供统一的数据访问接口
  • 隐藏底层数据存储细节
2.2.2 分层架构
project/
├── domain/              # 领域层
│   ├── models/          # 实体定义
│   ├── repositories/    # Repository接口
│   └── value_objects/   # 值对象
├── infrastructure/      # 基础设施层
│   ├── repositories/    # Repository实现
│   └── database/        # 数据库配置
├── application/         # 应用层
│   └── services/        # 应用服务
└── api/                 # 接口层
    └── handlers/        # HTTP处理器
2.2.3 完整实现示例

1. 定义 Repository 接口:

// domain/repositories/user_repository.go
type UserRepository interface {
    Create(ctx context.Context, tx interface{}, user *User) error
    Update(ctx context.Context, tx interface{}, user *User) error
    FindByID(ctx context.Context, tx interface{}, id uint) (*User, error)
    FindByEmail(ctx context.Context, tx interface{}, email string) (*User, error)
    WithTransaction(ctx context.Context, fn func(tx interface{}) error) error
}

2. 实现 Repository:

// infrastructure/repositories/user_repository_impl.go
type userRepositoryImpl struct {
    db *gorm.DB
}

func (r *userRepositoryImpl) Create(ctx context.Context, tx interface{}, user *User) error {
    db := r.getDB(tx)
    return db.WithContext(ctx).Create(user).Error
}

func (r *userRepositoryImpl) FindByID(ctx context.Context, tx interface{}, id uint) (*User, error) {
    db := r.getDB(tx)
    var user User
    err := db.WithContext(ctx).First(&user, id).Error
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, ErrNotFound
    }
    return &user, err
}

func (r *userRepositoryImpl) WithTransaction(ctx context.Context, fn func(tx interface{}) error) error {
    return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        return fn(tx)
    })
}

func (r *userRepositoryImpl) getDB(tx interface{}) *gorm.DB {
    if tx != nil {
        if gormTx, ok := tx.(*gorm.DB); ok {
            return gormTx
        }
    }
    return r.db
}

3. 应用服务层:

// application/services/user_service.go
type UserService struct {
    userRepo UserRepository
}

func (s *UserService) CreateUserWithProfile(ctx context.Context, req *CreateUserRequest) error {
    return s.userRepo.WithTransaction(ctx, func(tx interface{}) error {
        // 创建用户
        user := &User{Name: req.Name, Email: req.Email}
        if err := s.userRepo.Create(ctx, tx, user); err != nil {
            return err
        }
        
        // 创建用户档案
        profile := &Profile{UserID: user.ID}
        return s.userRepo.Create(ctx, tx, profile)
    })
}
2.2.4 Repository 模式优势
  1. 关注点分离:业务逻辑不依赖具体存储实现
  2. 可测试性高:易于进行单元测试和 Mock
  3. 可维护性强:数据访问逻辑集中管理
  4. 可扩展性好:轻松切换数据源(MySQL → PostgreSQL)
  5. 一致性保证:统一的数据访问规范

2.3 其他调用模式

2.3.1 链式调用法
func (u *User) CreateQuery(tx *gorm.DB) *gorm.DB {
    return tx.Create(u)
}

// 使用
err := db.Transaction(func(tx *gorm.DB) error {
    return user.CreateQuery(tx).Error
})
2.3.2 Context 存储法
type contextKey string
const txKey contextKey = "db_tx"

func SetTxToContext(ctx context.Context, tx *gorm.DB) context.Context {
    return context.WithValue(ctx, txKey, tx)
}

func (u *User) Save(ctx context.Context) error {
    tx := GetTxFromContext(ctx)
    return tx.Save(u).Error
}
2.3.3 接口抽象法
type DBExecutor interface {
    Create(value interface{}) *gorm.DB
    Save(value interface{}) *gorm.DB
}

func (u *User) Save(db DBExecutor) error {
    return db.Save(u).Error
}

三、嵌套事务问题与解决方案

3.1 问题分析

3.1.1 GORM 嵌套事务机制

GORM 使用保存点(SavePoint)实现嵌套事务:

BEGIN;                    -- 外层事务开始
SAVEPOINT sp1;            -- 内层事务开始(保存点)
-- 执行操作...
ROLLBACK TO sp1;          -- 内层事务回滚(回滚到保存点)
COMMIT;                  -- 外层事务提交
3.1.2 嵌套事务的三种情况

情况1:内层失败,外层继续

db.Transaction(func(tx1 *gorm.DB) error {
    return tx1.Transaction(func(tx2 *gorm.DB) error {
        // 内层失败,回滚到保存点
        return errors.New("inner failed")
    })
    // 外层可以继续执行
})

情况2:内层失败,外层回滚

db.Transaction(func(tx1 *gorm.DB) error {
    if err := tx1.Transaction(func(tx2 *gorm.DB) error {
        return errors.New("inner failed")
    }); err != nil {
        return err  // 外层也回滚
    }
    return nil
})

情况3:内层成功,外层失败

db.Transaction(func(tx1 *gorm.DB) error {
    // 内层成功
    tx1.Transaction(func(tx2 *gorm.DB) error {
        tx2.Create(&User{})  // 操作成功
        return nil
    })
    
    // 外层失败
    return errors.New("outer failed")
    // 结果:内层操作被回滚!
})

3.2 解决方案

3.2.1 明确事务边界(推荐)

提供带事务和不带事务的两个版本:

// UserService
// 版本1:内部管理事务(给Controller直接调用)
func (s *UserService) CreateUserWithTx(ctx context.Context, req *CreateUserRequest) error {
    return s.repo.WithTransaction(ctx, func(tx interface{}) error {
        return s.createUser(ctx, tx, req)
    })
}

// 版本2:不管理事务(给其他Service调用)
func (s *UserService) CreateUser(ctx context.Context, tx interface{}, req *CreateUserRequest) error {
    // 纯业务逻辑,使用传入的tx
    return s.createUserBusinessLogic(ctx, tx, req)
}

// OrderService 调用
func (s *OrderService) CreateOrder(ctx context.Context, order *Order) error {
    return s.repo.WithTransaction(ctx, func(tx interface{}) error {
        // 调用UserService的不带事务版本
        userReq := &CreateUserRequest{Name: order.UserName}
        if err := s.userService.CreateUser(ctx, tx, userReq); err != nil {
            return err
        }
        
        // 创建订单(使用同一个tx)
        return s.orderRepo.Create(ctx, tx, order)
    })
}
3.2.2 事务传播接口

通过 Context 传递事务对象:

func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) error {
    // 检查是否已有事务
    if tx := GetTxFromContext(ctx); tx != nil {
        // 已经在事务中,直接执行业务逻辑
        return s.createUserLogic(ctx, tx, req)
    }
    
    // 没有事务,开启新事务
    return s.db.Transaction(func(tx *gorm.DB) error {
        txCtx := SetTxToContext(ctx, tx)
        return s.createUserLogic(txCtx, tx, req)
    })
}
3.2.3 工作单元模式(Unit of Work)

最清晰的解决方案:

type UnitOfWork interface {
    Begin(ctx context.Context) (context.Context, error)
    Commit(ctx context.Context) error
    Rollback(ctx context.Context) error
    GetRepository(ctx context.Context) Repository
}

func (s *OrderService) CreateOrder(ctx context.Context, order *Order) error {
    uow := s.uowFactory.New()
    
    txCtx, err := uow.Begin(ctx)
    if err != nil {
        return err
    }
    defer uow.Rollback(txCtx)
    
    // 创建用户和订单使用同一个工作单元
    userService := NewUserService(uowFactory)
    if err := userService.createUserLogic(txCtx, uow, order.UserRequest); err != nil {
        return err
    }
    
    return uow.Commit(txCtx)
}
3.2.4 依赖注入事务对象

最简单实用的方式:

type UserService struct {
    db *gorm.DB
}

// 方法1:开启新事务
func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) error {
    return NewTransactionScope(s.db, func(tx *gorm.DB) error {
        return s.createUserInTx(ctx, tx, req)
    })
}

// 方法2:使用现有事务
func (s *UserService) CreateUserInTx(ctx context.Context, tx *gorm.DB, req *CreateUserRequest) error {
    // 纯业务逻辑,不开启事务
    // 可以安全地被其他Service调用
    return tx.Create(&User{Name: req.Name}).Error
}

3.3 最佳实践建议

3.3.1 架构分层原则
Controller
    ↓
Service Layer (管理事务边界)
    ↓
Repository Layer (无事务逻辑)
    ↓
Database
3.3.2 Service 层设计原则

原则1:一个Service方法只做一件事

// ✅ 好的设计:分离事务和业务逻辑
func (s *UserService) CreateUser(req *CreateUserRequest) error {
    return s.executeInTransaction(func(tx *gorm.DB) error {
        return s.createUserBusinessLogic(tx, req)
    })
}

func (s *UserService) createUserBusinessLogic(tx *gorm.DB, req *CreateUserRequest) error {
    // 纯业务逻辑
}

原则2:提供带事务和不带事务的版本

// 带事务的公开方法(供Controller调用)
func (s *UserService) CreateUserWithTransaction(req *CreateUserRequest) error {
    return s.db.Transaction(func(tx *gorm.DB) error {
        return s.createUser(tx, req)
    })
}

// 不带事务的内部方法(供其他Service调用)
func (s *UserService) createUser(tx *gorm.DB, req *CreateUserRequest) error {
    // 纯业务逻辑,不管理事务
}
3.3.3 项目规模选择建议
项目规模推荐方案说明
小型项目参数传递法简单直接,避免过度设计
中型项目完整 Repository结构清晰,易于维护
大型项目Repository + 工作单元支持复杂事务,高可测试性

四、总结

4.1 关键要点

  1. GORM 闭包事务适合简单场景,但嵌套事务需要特别注意
  2. Repository 模式提供良好的抽象,特别适合大型项目
  3. 嵌套事务问题的核心是事务边界管理
  4. 最佳实践是分离事务管理和业务逻辑

4.2 问题解答

Q: CreateUser 内部有事务,其他 Service 调用它会导致嵌套事务,内层事务会自己回滚吗?

A: 是的,但情况复杂:

  • 内层事务失败:回滚到保存点,错误传播到外层
  • 外层事务失败:整个事务回滚,包括内层"已提交"的操作
  • 最危险的情况:内层成功,外层失败,内层操作被回滚

4.3 最终建议

对于大多数项目,推荐使用 参数传递法简化版 Repository 模式

// 简单清晰的写法(推荐)
func CreateOrder(db *gorm.DB, order *Order, items []OrderItem) error {
    return db.Transaction(func(tx *gorm.DB) error {
        // 直接使用tx执行操作
        if err := tx.Create(order).Error; err != nil {
            return err
        }
        
        for _, item := range items {
            item.OrderID = order.ID
            if err := tx.Create(&item).Error; err != nil {
                return err
            }
        }
        
        return nil
    })
}

这种方式既保持了事务的清晰性,又避免了过度设计,适合大多数实际项目需求。

作者:Smoothcloud润云