go的gorm刚用起来比较玄幻。有时会触发多个db操作相互污染的问题。为了弄清这个问题特地研究了一下文档及源码。文档链接地址:https://gorm.io/zh_CN/docs/method_chaining.html
下面我尝试从源码角度理解一下gorm句柄切换的逻辑:
func (db *DB) getInstance() *DB {
// 句柄之间的转换关键在 db.clone,通过这个标识来确定怎么操作sql(所有操作db的逻辑都存放到这个属性里:db.Statement)
//clone > 0 时:需要新建一个db.Statement 实例,
if db.clone > 0 {
//这里得到的 tx.clone = 0
tx := &DB{Config: db.Config, Error: db.Error}
// =1 时代表需要重新定义一个句柄(新的db.Statement实例)(另外提示一下:通过db.Open 获取的句柄 db.clone = 1)
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 {// > 1时,代表需要从原来的db.Statement clone 一个,(另外提示一下:执行db.Session(&gorm.Session{}) 的时候,db.clone = 2,将用到这里的逻辑)
// with clone statement
tx.Statement = db.Statement.clone()
tx.Statement.DB = tx
}
return tx
}
// db.clone = 0时,直接返回
return db
}
可以理解为,clone > 0 时,每次第一次操作数据库后,就会返回一个新的db 句柄,这个新的句柄会保留所有在这个句柄上的操作,直至调用 db.Session(&gorm.Session{})时, 会保留之前的操作,然后会继续链上下次的操作。
在重用db句柄时要注意这个逻辑,特别在事务中,必须重用一个db实例,否则就不是在一个事务中了。这个时候就要小心sql相互污染的问题,比较好的习惯是“一链到底”,如非必要别再次赋值。如下:
//Db() 方法获取gorm.Open() 返回的单例
db := Db().Begin()
//这个db 已经和上一行的db不是一个实例了
db = db.Where("type = 2")
if s, found := getData["status"]; found {
//这个db 和 第二个 db 是一个实例
db = db.Where("status = ?", s)
}
优化之后的代码就看起来比较简单了
//这里换一个名字
tr := Db().Begin()
//从tr获取一个db句柄
dbDoSomething1 = tr.Where("type = 2")
if s, found := getData["status"]; found {
//直接操作db句柄
dbDoSomething1.Where("status = ?", s)
}