Gorm 的黑魔法
小伙看到同事写的Gorm黑魔法,逐渐不淡定了。
开发过程中,看到同事的代码写了这么一段:
db = db.Session(&gorm.Session{Context: db.Statement.Context}).FirstOrCreate(&entity)
if db.Error !=nil{
return components.ErrorDbInsert.WrapPrintf(db.Error, "Insert error, entity:%s", utils.ToJson(entity))
}
if db.RowsAffected == 0 {
return components.ErrorAlreadyExist
}
FirstOrCreate
我不禁感到疑惑,gorm
的 RowsAffected
在进行查询,如果查到数据,也是有值的,为什么在这里可以用 RowsAffected == 0
来判断数据已存在?
抱着这个疑问,我点开了 FirstOrCreate
的代码:
func (db *DB) FirstOrCreate(dest interface{}, conds ...interface{}) (tx *DB) {
queryTx := db.Limit(1).Order(clause.OrderByColumn{
Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},
})
if tx = queryTx.Find(dest, conds...); queryTx.RowsAffected == 0 {
...
return tx.Create(dest)
} else if len(db.Statement.assigns) > 0 {
...
return tx.Model(dest).Updates(assigns)
}
return db
}
我们可以很容易地发现,在 Find
查到数据且 assigns
没有值的情况下,return
的是 db
,而其他情况下 return
的是 tx
。直觉告诉我,原因大概率在这个上面。
getInstance()
Limit、Order、Find
等许多函数都调用了同一个函数 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 = 0
时,不做处理,返回db
;clone > 0
且clone = 1
时,返回一个新的db
(clone
变成了0),statement
中,连接池和上下文延用之前的,把条件和变量置为空;clone > 0
且clone > 1
(目前只有clone
为2)时,返回一个新的db
(clone
变成了0),statement
完全复制之前的db
;
db.getInstance()
的小结:
- 如其名,这个函数的作用是获取一个
db
实例; - 获取的
db
跟原db
的clone
属性直接相关: clone = 0
,获取当前db
实例,即不做处理;clone = 1
,返回一个新的db
,并清空查询条件;clone = 2
,返回一个新的db
,不清空查询条件;- 不管是那种克隆模式,都不会修改实例的连接池和上下文;
揭秘黑魔法
看到这里,这个“黑魔法”的原理已经呈现在我们眼前:
在调用 FirstOrCreate
函数的时候,如果此时 clone
不为0,则会在调用 Limit
函数的时候生成一个新的实例tx
。
tx
与 db
是两个不同的实例, Find
查到的数据不为空时,tx
的 RowsAffected
会变化,而 db
的 RowsAffected
仍然不变。
因此在查到数据且不进行 Update
的情况下,函数会直接返回db
,而RowsAffected
为 0。
基于上述理论,大胆猜想 Session
这个函数必定会改变 db
的 clone
属性,查看 Session
源码后,我如愿找到了:
//Session create new db session
func(db *DB) Session(config *Session) *DB {
var(
txConfig = *db.Config
tx = &DB{
Config: &txConfig,
Statement: db.Statement,
Error: db.Error,
clone: 1, // 设置 clone 的默认值为1
}
)
...
// 回顾 getInstance 函数对 clone = 2 时的处理,NewDB 的含义不言而喻(再次吐槽魔法值)
if !config.NewDB {
tx.clone = 2
}
...
return tx
}
后记
发现在新版本的 gorm
,金柱大佬直接把“魔仙棒”给没收了,不管结果如何,都返回tx
,因此新版本(v1.23.0
之后)的gorm
将无法使用这个“黑魔法”:
// FirstOrCreate finds the first matching record, otherwise if not found creates a new instance with given conds.
// Each conds must be a struct or map.
func (db *DB) FirstOrCreate(dest interface{}, conds ...interface{}) (tx *DB) {
tx = db.getInstance()
queryTx := db.Session(&Session{}).Limit(1).Order(clause.OrderByColumn{
Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},
})
if result := queryTx.Find(dest, conds...); result.Error == nil {
if result.RowsAffected == 0 {
...
return tx.Create(dest)
} else if len(db.Statement.assigns) > 0 {
...
return tx.Model(dest).Updates(assigns)
}
} else {
tx.Error = result.Error
}
return tx
}
事实上,这个“黑魔法”的使用是不符合 RowsAffected
的原本定义的,开发者把它当成一个bug优化掉也是理所当然,实际上的开发应当尽量不用。
“黑魔法”应当少用,但是值得探究,这可比生啃源码有趣得多。