GORM V2 几个最实用的功能和升级注意事项

2024-06-26 12:38:20 浏览数 (1)

最近在自己在开发个人的新项目,这个项目预计未来几个月后能跟大家见面,项目搭建的过程中遇到了ORM版本选择的问题,经过自己仔细斟酌还是选择了GORM的 V2版作为项目的ORM框架,这个抉择过程其实就是说服自己不使用的V1的一个心里斗争。

因为这几年在公司做的项目都是使用的GORM的V1版本,如果选择V1的话我只要把以前总结的那些代码拿过来改改就能用了,但是因为两个原因还是选择了使用GORM V2,下面我先重点说一下这两个原因,再介绍几个使用V2版本时大家写代码需要注意的破坏性更新。

V2 支持在日志中增加追踪信息

说实话这个是我选择升级到V2的一个主要原因, 良好的基础框架是一个项目成功的必备因素,GORM V1版本开发的Logger接口中我们是没有办法把请求上下文传递进去的。

在使用GORM的时候,如果我们想把GORM产生的日志记录到项目统一的应用日志中的时,需要自己去实现GORM提供的logger 接口。

V1版本的GROM的 logger 接口长这个样子,仅仅提供了一个Print方法,Print方法的参数都是 create、updates、query 这些方法的回调中传递过去的,我们并没有办法传递Context。

代码语言:javascript复制
type logger interface {
 Print(v ...interface{})
}

那么你想用常规方法把请求的traceId 记录到GORM 生成的日志中是完全不可能实现的,只能借助一些非常规的方法,比如引入一个GLS开源库,每个请求唯一的traceid、spanid 这些都放到gls里,记日志的时候再从GLS里把这些信息拿出来记录到日志中去。

一般都不推荐引入GLS,实际上性能影响不明显,之前有些服务请求量最大2000QPS的时候也没出现过瓶颈。不过现在Github上star最多的那个GLS库在 Go 1.20 版本以后已经不能用了,不然我们之前公司的那些项目用的Go版本就不会卡在1.19啦 笑死╮(╯▽╰)╭

在GORM V2 中它新增了以下Logger 接口:

代码语言:javascript复制
type Interface interface {  
   LogMode(LogLevel) Interface  
   Info(context.Context, string, ...interface{})  
   Warn(context.Context, string, ...interface{})  
   Error(context.Context, string, ...interface{})  
   Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)  
}

每个方法都有应用的上下文Context参数传递进来,还专门提供了Trace方法让我们实现,供我们实现查询的SQL语句和消耗时间的记录。

当我们自己实现好GORM的Logger后,在GORM创建连接的时候需要把Logger选项配置成我们自定义Logger

代码语言:javascript复制
db, err := gorm.Open(
     mysql.Open(
       cfg.Dsn, &gorm.Config{
         Logger: MyGormLogger
       })
)

在使用GORM执行查询的地方,通过withContext 带上Context 信息即可

代码语言:javascript复制
func (dao *userDao) AddUser(c context.Context, user *model.User) (userId int64, err error) { 
   err = db.GetConn().WithContext(c).Create(user).Error  
   ....
}

关于怎么自定义实现GORM Logger 把GORM 日志统一整合到项目应用日志,未来等我项目成型了再跟大家分享。接下来说下第二个让我决定使用GORM V2 的原因

CREATE方法支持批量创建模型

在GORM V1版本里,模型本身是不在带批量创建的功能的,想要批量创建一种选择是写个循环,在循环里调用模型的Create方法。

还有一种是使用db.Raw 或者 db.Exec 执行手写的SQL来进行批量创建,我以前每次需要批量创建模型是都会手动在模型里定义一个BulkCreate方法

代码语言:javascript复制
func BulkInsertOrderGoods(unsavedRows []*table.OrderGoods) error {
 valueStrings := make([]string, 0, len(unsavedRows))
 valueArgs := make([]interface{}, 0, len(unsavedRows)*3)
 for _, row := range unsavedRows {
  valueStrings = append(valueStrings, "(?, ?, ?)")
  valueArgs = append(valueArgs, row.UserId)
  valueArgs = append(valueArgs, row.GoodsName)
  valueArgs = append(valueArgs, row.OrderId)
 }
 statement := fmt.Sprintf("INSERT INTO " table.OrderGoods{}.TableName() " (user_id, goods_name, order_id) VALUES %s",
  strings.Join(valueStrings, ","))

 err := DB().Exec(statement, valueArgs...).Error
 return err
}

还得时刻注意,尽量让程序拼接SQL时不出错。

那么在GORM V2 里,我们只需要把模型对象的把模型切片传给模型的Create方法

代码语言:javascript复制
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)

for _, user := range users {
  user.ID // 1,2,3
}

或者是使用gorm.DB对象上的方法 CreateInBatches 来指定批量插入的批次大小

代码语言:javascript复制
var users = []User{{Name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}}

// batch size 100
db.CreateInBatches(users, 100)

CreateInBatches方法需要在初始化GORM的时候指定对应的配置,推荐还是用第一种方法。另外更新或者插入方法Upsert 在V2也支持批量操作。

我觉得有了这两个特性,在新搭建项目的时候很难不选择使用V2版本,第一个特性让用日志排查问题变得更简单,第二个特性能让不用再去自己写代码实现批量操作。

接下来说几个破坏性更新,这个可能是从V1 升级到 V2的障碍

需要注意的几个破坏性更新

初始化方式变更

GORM V1 和 V2 用到的初始化Open方法发生了变更

代码语言:javascript复制
/ jinzhu
func Open(dialect string, args ...interface{}) (db *DB, err error) {}

// grom.io
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {}

此外还有一些设置连接的方式也有微调,我把V1和V2 初始化的Demo 放在这里大家可以比较一下,首先是V1版本的

代码语言:javascript复制
 db, err := gorm.Open(config.Database.Type, config.Database.DSN)
 if err != nil {
  panic(err)
 }
 db.DB().SetMaxOpenConns(config.Database.MaxOpenConn)
 db.DB().SetMaxIdleConns(config.Database.MaxIdleConn)
 db.DB().SetConnMaxLifetime(config.Database.MaxLifeTime)

下面是V2版本的

代码语言:javascript复制
 db, err := gorm.Open(
  mysql.Open(option.DSN),
  &gorm.Config{
   Logger: NewGormLogger(),
  },
 )
 if err != nil {
  panic(err)
 }
 sqlDb, _ := db.DB()
 sqlDb.SetMaxOpenConns(option.MaxOpenConn)
 sqlDb.SetMaxIdleConns(option.MaxIdleConn)
 sqlDb.SetConnMaxLifetime(option.MaxLifeTime)

Find 查不到数据时不再返回Error

使用Find查询数据的时候,在V1版本里如果查不到数据会返回错误,所以很多人在代码里的下面这行判断会失效

代码语言:javascript复制
if err != gorm.ErrRecordNotFound

但是使用 FirstLastTake 这些预期会返回结果的方法查询记录时,还会返回 ErrRecordNotFound

软删除支持更多模式

说到这里,发现有个很好的特性在上面忘记说了,汗。。。 那就在这里在补充一下吧,GORM自带的软删除我之前是不会用的,因为它那个字段名还有字段的默认值都是限定不能改的,默认值NULL,这在很多公司里DBA设置的约束里是不允许的。

所以我之前没有使用过。但是现在GORM V2 支持Flag 模式了,就是咱们很多人用的0代表未删除 1代表删除

使用前需要先安装GORM的soft_delete这个包。

代码语言:javascript复制
go get -u "gorm.io/plugin/soft_delete"

在定义模型时像下面这样给标记软删除的字段加上这个tag

代码语言:javascript复制
type User struct {
  ID    uint
  Name  string
  IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}

那么这样GORM在执行SQL语句时就会自动带上is_del这个字段进行查询啦

代码语言:javascript复制
// Query
SELECT * FROM users WHERE is_del = 0;

// Delete
UPDATE users SET is_del = 1 WHERE ID = 1;

这个我觉得也很好用。

是否要升级V2

这里说的这些知识我目前体验下来的几点明显变化,像什么数据迁移之类的我就没看,因为真正做项目的时候没这个权限。

大家觉得有必要从V1升级到V2吗,反正我负责的这些祖传老项目我是不敢动的,新项目倒是可以无脑选择V2。

咱们有踩过V1升V2版本的坑么,可以在评论区里说说呀。


0 人点赞