当gorm遇见generic

2023-07-12 15:59:34 浏览数 (2)

2022年3月15日,争议巨大但同时也备受期待的泛型终于伴随着Go1.18发布了。

这里我们并不打算讨论Go作为一门现代语言为啥需要泛型(前辈资深程序员“左耳朵耗子”有这方便深刻的探讨,感兴趣的人可自行google ),也不纠结Go在泛型在实现上是否足够优雅和其争议性,更不会详解Go 泛型的使用教程和原理。我们只是试图去降低重复的CRUD的代码,在Gorm的基础上进一步封装db 存储层的逻辑,构造一个媲美PHP doctrine的组件。

Talk is cheap. Show me the code

整体代码目录结构

代码语言:json复制
├── gorm 

│   ├── gorm.go  // db连接初始化,db相关错误码,存储接口和泛型 repository 实现    

│   ├── coupon.go // 优惠券

│   ...

│   ├── xxx.go // 其他

定义存储对象接口

代码语言:go复制
// Model PO interface

type Model[E any] interface {

    // TableName 数据库表名

    TableName() string

    // ToEntity PO to BO

    ToEntity()*E

    // FromEntity BO to PO

    FromEntity(entity E) interface{}

}

这里 db po 需实现该接口。ToEntity 和 FromEntity 是由于我们这个项目本身采用的是DDD的架构,在domain层定义实体,其他项目结构如:经典的 MVC,可定义适合本项目的 interface。

存储 repository 泛型实现 (DAO实现)

代码语言:go复制
type repository[M Model[E], E any] struct {
   db *gorm.DB
}

// NewRepository 新建仓库存储对象
func NewRepository[M Model[E], E any]() *repository[M, E] {
   var model M
   tableName := model.TableName()
   return &repository[M, E]{db: db.Table(tableName)}
}

// ADD 插入一条记录
func (r *repository[M, E]) ADD(ctx context.Context, entity E) (*E, error) {
   var m M
   model := m.FromEntity(entity).(M)
   err := r.db.WithContext(ctx).Create(&model).Error
   if err != nil {
      log.ErrorContextf(ctx, "ADD err: %v", err)
      return nil, ErrMysqlCommon
   }
   return model.ToEntity(), nil
}

// Info 根据ID获取记录
func (r *repository[M, E]) Info(ctx context.Context, id uint64) (*E, error) {
   var m M
   err := r.db.WithContext(ctx).Where("id = ?", id).Take(&m).Error
   if err != nil {
      log.ErrorContextf(ctx, "Get err: %v", err)
      return nil, ErrMysqlRecordNotFound
   }
   return m.ToEntity(), nil
}

// List 查询
func (r *repository[M, E]) List(ctx context.Context, query map[string]interface{}) ([]*E, error) {
   var models []M
   err := r.db.WithContext(ctx).Where(query).Find(&models).Error
   if err != nil {
      log.ErrorContextf(ctx, "Query err: %v", err)
      return nil, err
   }

   var ret []*E
   for _, m := range models {
      ret = append(ret, m.ToEntity())
   }
   return ret, nil
}

// Update 根据ID更新, SelectFields需要更新的字段
func (r *repository[M, E]) Update(ctx context.Context, id uint64, selectFields []string, entity E) (*E, error) {
   var m M
   model := m.FromEntity(entity).(M)
   err := r.db.WithContext(ctx).Where("id = ?", id).Select(selectFields).Updates(&model).Error
   if err != nil {
      log.ErrorContextf(ctx, "Updates err: %v", err)
      return nil, err
   }
   return model.ToEntity(), nil
}

这里我们实现CURD的泛型函数。其他项目可能有更复杂的表设计,可自行添加符合自己需求的方法。我们定义设计的 sql 表单是反 sql 范式的,新业务也没有历史包袱,没有复杂的 sql 操作,这些简单的方法已满足我们的需求。

优惠券实现

代码语言:go复制
// BaseModel model通用字段
type BaseModel struct {
   ID         uint64    `gorm:"column:id"`                  
   CreatedAt time.Time `gorm:"<-:false;column:created_at"` // gorm readonly
   UpdatedAt time.Time `gorm:"<-:false;column:updated_at"` // gorm readonly
}

// Coupon 优惠券
type Coupon struct {
   BaseModel
   ID uint64 `gorm:"column:id"`
   Name     string `gorm:"column:name"`
   // do some here
}

func (c Coupon) TableName() string {
   return "coupon"
}

func (c Coupon) ToEntity() *domain.Coupon {
   return &domain.Coupon{
      // do some here
   }
}

func (c Coupon) FromEntity(coupon domain.Coupon) interface{} {
   return Coupon{
      // do some here
   }
}

// NewCouponRepository 新建优惠券卷 db 存储对象
func NewCouponRepository() domain.CouponRepository {
   return NewRepository[Coupon, domain.Coupon]()
}

这里我们使用优惠券coupon db 存储示例,其他新增的存储DTO都只需简单实现 Model 泛型接口。

总结

泛型并不取代Go1.18之前用接口 反射实现的动态类型,我们并不需要刻意使用泛型而泛型,而是当你需要针对不同类型书写同样的逻辑才考虑泛型。 这里我们利用泛型实现 db 存储的简单封装,避免CURD代码的重复开发。其实可以单独封装出一个公共的组件库,方便其他项目使用,在实现细节上也有很多地方细节还需考虑。

0 人点赞