前言
之前我写过一篇文章golangWeb项目搭建,记录go+gin+gorm项目中的mvc架构设计,当时对于gorm的事务封装处理不够好,后面我对其进行了优化,在这篇文章中进行记录
原来的做法
原来我是参考到gorm框架的手动管理事务方式来定义事务接口。这样做我想到两个缺点
-
- Transaction接口的定义和gorm框架耦合太强
-
- 手动管理事务过于麻烦,容易出错
下面是原来做法的代码
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依赖注入是毒药还是解药?