Golang MVC 架构设计:封装 GORM 事务以简化数据库操作

前言

之前我写过一篇文章golangWeb项目搭建,记录go+gin+gorm项目中的mvc架构设计,当时对于gorm的事务封装处理不够好,后面我对其进行了优化,在这篇文章中进行记录

原来的做法

原来我是参考到gorm框架的手动管理事务方式来定义事务接口。这样做我想到两个缺点

    1. Transaction接口的定义和gorm框架耦合太强
    1. 手动管理事务过于麻烦,容易出错

下面是原来做法的代码

package repositories

import "gorm.io/gorm"

type Transaction interface {
	Begin() *gorm.DB
	Commit(tx *gorm.DB) error
	Rollback(tx *gorm.DB) error
}

type GormTransaction struct {
	db *gorm.DB
}

func NewGormTransaction(db *gorm.DB) *GormTransaction {
	return &GormTransaction{db: db}
}

func (g *GormTransaction) Begin() *gorm.DB {
	tx := g.db.Begin()
	return tx
}

func (g *GormTransaction) Commit(tx *gorm.DB) error {
	return tx.Commit().Error
}

func (g *GormTransaction) Rollback(tx *gorm.DB) error {
	return tx.Rollback().Error
}

优化思路

后面我了解到gorm框架中也有自动管理事务的方式,如下

	func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error)

通过这个函数,我们就不需要手动通过调用gorm的begin函数来创建事务管理对象,在fc func(tx *DB) error中我们只需要执行数据库操作即可,如果出错则return err即可,Transaction函数会自动帮我们回滚操作。

在原来的方式中,事务对象通过begin函数创建并且通过明文参数来传递,我对其做了一个优化,就是将其放在context上写文中,每次业务Repository执行sql时都要检测context是否存在tx事务对象,如果存在则使用tx来执行sql,从而实现统一管理。

代码实现

首先我们不再直接将gorm.DB放入业务相关的Repository中,而是封装在一个基础的BaseRepository, 让所有业务Repository继承BaseRepository。业务Repository必须要通过调用BaseRepository.DB(WithContext(context))获取gorm.DB对象来执行sql。BaseRepository.DB会尝试从context中contextKey 获取事务管理对象,如果不存在则说明是普通的业务,则返回BaseRepository .db来执行sql。

type DBOption func(*gorm.DB) *gorm.DB

type BaseRepository struct {
	db *gorm.DB
}

func NewBaseRepository(db *gorm.DB) *BaseRepository {
	return &BaseRepository{
		db: db,
	}
}

// 智能获取 DB 实例
func (r *BaseRepository) DB(opts ...DBOption) *gorm.DB {
	db := r.db
	for _, opt := range opts {
		db = opt(db)
	}
	return db.Debug()
}

// 上下文选项
func WithContext(ctx context.Context) DBOption {
	return func(db *gorm.DB) *gorm.DB {
		if tx, ok := ctx.Value(txKey).(*gorm.DB); ok {
			return tx.WithContext(ctx)
		}
		return db.WithContext(ctx)
	}
}

接下来封装func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error) 。Service层只要像使用gorm的Transcation方法一样调用Transaction(ctx context.Context, fn func(context.Context) error) error就能实现事务管理,十分方便

type contextKey struct{}

var txKey = contextKey{}

type TransactionInterface interface {
	Transaction(ctx context.Context, fn func(context.Context) error) error
}

// GORM 事务管理器实现(实现细节在基础设施层)
type GormTransactionManager struct {
	db *gorm.DB
}

var _ TransactionInterface = (*GormTransactionManager)(nil)

func NewGormTransactionManager(db *gorm.DB) TransactionInterface {
	return &GormTransactionManager{
		db: db,
	}
}

func (m *GormTransactionManager) Transaction(ctx context.Context, fn func(context.Context) error) error {
	return m.db.Transaction(func(tx *gorm.DB) error {
		txCtx := context.WithValue(ctx, txKey, tx)
		return fn(txCtx)
	})
}

使用示例1—— Service层开启事务

type FocusService struct {
	logger          *zap.Logger
	config          configs.Config
	db              *gorm.DB
	tokenMaker      utils.TokenMaker
	redis           *redis.Client
	focusRepository repositories.FocusRepositoryInterface
	tagRepo         repositories.TagRepositoryInterface
	transaction     repositories.TransactionInterface
}

func (s *FocusService) CreateFocus(ctx *gin.Context, focusName string) (httpcode int, vo models.Focus, err error) {
	vo.FocusName = focusName
	err = s.transaction.Transaction(ctx, func(txCtx context.Context) error {
		// 创建对应的标签
		tag, err := s.tagRepo.CreateTag(txCtx, focusName)
		if err != nil {
			if strings.Contains(err.Error(), "unique") {
				return nil // 如果已存在则不理会
			}
			s.logger.Error("创建标签失败", zap.Error(err))
			return err
		}

		vo.TagId = tag.TagId
		err = s.focusRepository.CreateFocus(txCtx, &vo)
		if err != nil {
			if strings.Contains(err.Error(), "unique") {
				s.logger.Error("关注点已存在", zap.Error(err))
				return repositories.ErrDuplicate
			}
			s.logger.Error("创建关注点失败", zap.Error(err))
			return err
		}

		return nil
	})

	if err != nil && err == repositories.ErrDuplicate {
		return http.StatusConflict, models.Focus{}, errors.New("关注点已存在")
	}

	return http.StatusOK, vo, nil
}

可以看到,在service层根本就不需要管理事务的提交,比以前的方式轻松多了。
使用示例2—— Repository层开启事务

type TransactionRepositoryInterface interface {
	CreateVideoRewardTransaction(ctx context.Context, senderId int, reciverId int, amount int, relatedId int) error
}

type TransactionRepository struct {
	*BaseRepository
}

var _ TransactionRepositoryInterface = (*TransactionRepository)(nil)

func NewTransactionRepository(db *gorm.DB) TransactionRepositoryInterface {
	return &TransactionRepository{
		BaseRepository: NewBaseRepository(db),
	}
}

func (r *TransactionRepository) CreateVideoRewardTransaction(ctx context.Context, senderId int, reciverId int, amount int, relatedId int) error {
	return r.DB(WithContext(ctx)).Transaction(func(tx *gorm.DB) error {
		err := tx.Create(&models.Transaction{
			UserId:    senderId,
			TradeType: constant.TRADETYPE_VIDEO_REWARD,
			Amount:    -amount,
			RelatedId: relatedId,
		}).Error
		if err != nil {
			return err
		}

		err = tx.Create(&models.Transaction{
			UserId:    reciverId,
			TradeType: constant.TRADETYPE_VIDEO_REWARD,
			Amount:    amount,
			RelatedId: relatedId,
		}).Error
		if err != nil {
			return err
		}

		return nil
	})

}

后续优化

我有阅读到golang也有自己的依赖注入相关的库如Dig,后续我会尝试使用,敬请期待吧

后话

过度设计只会让代码变得更难理解,golang本来就是为了开箱即用,所以各位进行封装时一定要考虑其必要性。我正在开发的项目到了目前这个阶段,其实我个人觉得确实有点过度封装了,而且要是有新人加入项目会有一些难度。对于这个问题可以阅读此文章在Golang依赖注入是毒药还是解药?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值