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

比如WhereLimitSelectTablesJoinClauses等等,这些在语句执行被执行前,设置和修改语句内容的,都叫 Chain Method

Finisher Method

比如CreateFirstFindTakeSaveUpdate``DeleteScanRowRows等等,会设置和修改语句内容,并执行语句的,都叫 Finisher Method。

New Session Method

比如SessionWithContextDebug 这三个方法,他们会新建一个Session。WithContextDebug 都只是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{}.NewDBfalse,则为返回的*gorm.DB 实例clone字段是2,如果为true,则为1。
  • 在调用(*gorm.Gorm).Session()时,如果Session{}.Initializedtrue,则返回的*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之间用空格隔开.

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐