gorm v1 升级到v2 注意事项及分析
Count() 入参类型变化
V1 : Count(int)
V2: Count(int64)
Limt()\Offset()
入参类型由 interface{} -> int
Order
v1: 参数为空字符串会被忽略,从而正常执行: Order("") 生成的sql中不带order by
v2: 空字符串也会被生成 order by 语句,从而导致 sql 执行失败。需要自行判空
Find
在 V1 版本中, Find(&User{}) 当记录不存在时会返回 ErrRecordNotFound
在 V2 版本中,Find(&User{}) 不会返回 ErrRecordNotFound
查询条件复用
V1示例:
query := db.Where("ctime > 123123123")
// 查询1
query.Offset((page-1) * pageSize).Limit(10).Find(&users)
// 查询2
query.Count(&count)
正确的V2版本 (复用条件时,需要带上 .Session(&grom.Session{}),否则会出现查询2里面带有查询1的条件,在并发情况下还会出现线程安全问题) :
query := db.Where("ctime > 123123123")
// 查询1
query.Session(&gorm.Session{}).Offset((page-1) * pageSize).Limit(10).Find(&users)
// 查询2
query.Session(&gorm.Session{}).Count(&count)
原因分析
Chain Method
比如Where、Limit、Select、Tables、Join、Clauses等等,这些在语句执行被执行前,设置和修改语句内容的,都叫 Chain Method
Finisher Method
比如Create、First、Find、Take、Save、Update``Delete、Scan、Row、Rows等等,会设置和修改语句内容,并执行语句的,都叫 Finisher Method。
New Session Method
比如Session、WithContext、Debug 这三个方法,他们会新建一个Session。WithContext和Debug 都只是Session方法特定调用的简写,底层都是调用的Session方法。
Statement
每个*gorm.DB 实例都会有一个Statement的字段,Statement就是我们真正要执行的语句,我们的 Chain Method 和 Finisher Method,事实上都是在修改Statement这个结构体。最后这个结构体会被渲染为SQL语句。
gorm的并发模型
首先,我们需要先去理解几乎每个方法中都会调用的函数:tx = db.getInstance()。

func (db *DB) getInstance() *DB { if db.clone > 0 { tx := &DB{Config: db.Config, Error: db.Error} if db.clone == 1 { // clone with new statement tx.Statement = &Statement{ DB: tx, ConnPool: db.Statement.ConnPool, Context: db.Statement.Context, Clauses: map[string]clause.Clause{}, Vars: make([]interface{}, 0, 8), } } else { // with clone statement tx.Statement = db.Statement.clone() tx.Statement.DB = tx } return tx } return db }
当clone=1时,这个*gorm.DB 实例总是并发安全的,因为它总是会返回一个全新的*gorm.DB 实例,不会对老*gorm.DB 实例有什么读写。
当clone=2时,这个*gorm.DB 实例也总是并发安全的,因为任何的 Chain Method 和 Finisher Method 都只会去读和复制当前*gorm.DB 实例的值,而不会修改,因此只会对这个*gorm.DB 实例并发读,那么当然是并发安全的。
当clone=0时,这个*gorm.DB 实例就不并发安全。
那clone字段分别会在什么情况下等于0、1、2呢?
- 在使用
gorm.Open()之后,新建出来的*gorm.DB实例clone字段总是1。 - 在调用
(*gorm.Gorm).Session()时,如果Session{}.NewDB为false,则为返回的*gorm.DB实例clone字段是2,如果为true,则为1。 - 在调用
(*gorm.Gorm).Session()时,如果Session{}.Initialized为true,则返回的*gorm.DB实例clone字段是0。这条规则优先级高于Session.NewDB。 - 在调用了任意Chain Method、Finisher Method之后,返回的
Gorm对象clone字段是0。
结论:
- 使用
gorm.Open()创建出来的对象,完全无法被修改。因为对他调用任何方法,最后都只会创建出新的*gorm.DB实例。所以不妨称为connection,简称为conn。 - 使用
conn.Session()新建一个*gorm.DB实例来查询,和直接使用conn来查询,效果是一样的。因为conn.Session()也只会clone一个空的Statement。 - 如果想让之后的查询都带上特定的条件,那么需要先设定好初始的条件,再使用 New Session Method 来创建新的
*gorm.DB实例,并且之后的查询都使用这个*gorm.DB实例。比如db.Unscoped().Session(&gorm.Session{})。 - 无论如何不能并发调用 Chain Method 和 Finisher Method,要么会查询条件污染,要么查询条件丢失,那么还可能会panic。
提示“define a valid foreign key for relations or implement the Valuer/Scanner interface”
位置:结构体里想加入一些额外的字段,作为输出用,而不需要建表,比如嵌入的结构体,所嵌入的是数组就提示这个了。
//模板-输入表
type TempleInputs struct {
gorm.Model
UserTempleID uint
InputAlias string `json:"inputalias" gorm:"column:input_alias"`
RealMin string `json:"realmin"`
RealMax string `json:"realmax"`
HistoryInputValue HistoryInputValue `json:"historyinputvalue"`
SelectValue []Select2 `json:"selectvalue" gorm:"-"` // hasMany 不加 gorm:"-"这个就会提示错误了。因为查询表的时候,是没有这个表存在的
TextArealValue TextAreal `json:"textarealvalue" gorm:"-"`// hasone
}
// 这儿结构体并不需要建表,仅仅是为了前端显示的时候,构造这个数据加入到上面那个结构体中
type Select2 struct {
ID uint `json:"id"`
Text string `json:"text"`
Value string `json:"value"`
}
解决方案,加 gorm:"-"。注意,json和gorm之间用空格隔开.
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)