最近在自己在开发个人的新项目,这个项目预计未来几个月后能跟大家见面,项目搭建的过程中遇到了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
db, err := gorm.Open(
mysql.Open(
cfg.Dsn, &gorm.Config{
Logger: MyGormLogger
})
)
在使用GORM执行查询的地方,通过withContext
带上Context 信息即可
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
方法
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
来指定批量插入的批次大小
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
方法发生了变更
/ 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
但是使用 First
、Last
、Take
这些预期会返回结果的方法查询记录时,还会返回 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版本的坑么,可以在评论区里说说呀。