gorm源码之db的克隆
前置知识简介:链式调用与方法
gorm中,方法是用来进行具体处理逻辑的函数。
gorm中的方法有三种,Chain Method, Finisher Method, New Session Method.
-
chain method可以用来将特定筛选条件增加到数据库的状态中;
常见的有db.where,db.select等
-
finisher method可以立即执行回调,生成并执行sql语句。
比如create,first,find等
-
new session method用于新建会话,相当于将db的状态更新。
gorm通过链式的方式允许我们调用这些函数,也就是说你可以很方便的用
db.where().Find()
这样的方式完成代码,而不必一层套一层的函数( A(B(param a,param b)) )
值得注意的是,chain method与finisher method都会返回一个新的db实例
db的clone
我们先来看一下下面的两段代码
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// db is a new initialized `*gorm.DB`, which is safe to reuse
db.Where("name = ?", "jinzhu").Where("age = ?", 18).Find(&users)
db.Where("name = ?", "jinzhu2").Where("age = ?", 20).Find(&users)
db.Find(&users)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// db is a new initialized *gorm.DB, which is safe to reuse
tx := db.Where("name = ?", "jinzhu")
tx.Where("age = ?", 18).Find(&users)
tx.Where("age = ?", 28).Find(&users)
这两段代码,各自打开了一个数据库,有筛选条件,有查询操作,不同的是,第一段代码自始至终都只用db这一个数据库实例,而第二段后面都在用tx这一个db.where返回的db新实例进行查询操作。
你知道这两段代码中的查询操作翻译成sql语句是什么吗?
思考三秒钟,以下是答案:
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// db is a new initialized `*gorm.DB`, which is safe to reuse
db.Where("name = ?", "jinzhu").Where("age = ?", 18).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18;
db.Where("name = ?", "jinzhu2").Where("age = ?", 20).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu2' AND age = 20;
db.Find(&users)
// SELECT * FROM users;
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// db is a new initialized *gorm.DB, which is safe to reuse
tx := db.Where("name = ?", "jinzhu")
tx.Where("age = ?", 18).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18
tx.Where("age = ?", 28).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18 AND age = 28;
相信几乎所有人都能得到第一段代码的正确结果,而对于第二段代码,我们惊奇地发现,select语句中的查询条件居然包含了之前代码中所用到的。
gorm官方文档指出了这一现象,很贴心地说这一种方式使得数据库实例不可reuse,毕竟查询条件都被污染了,而要究其根底,还得去翻阅源码。
让我们先看一下DB的结构
//gorm.go
type DB struct {
*Config
Error error
RowsAffected int64
Statement *Statement
clone int
}
其中statement用来存储和该DB变量状态相关的信息,我们前面说过的where等查询条件,就和statement这个量密切相关。
我们重点放在这个clone上。
可以先看一下初次用这个db实例,该clone变量会变为多少
以下是open函数的源码的一部分:
// Open initialize db session based on dialector
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
//
//
db = &DB{Config: config, clone: 1}
db.callbacks = initializeCallbacks(db)
if config.ClauseBuilders == nil {
config.ClauseBuilders = map[string]clause.ClauseBuilder{}
}
if config.Dialector != nil {
err = config.Dialector.Initialize(db)
if err != nil {
if db, err := db.DB(); err == nil {
_ = db.Close()
}
}
}
if config.PrepareStmt {
preparedStmt := NewPreparedStmtDB(db.ConnPool)
db.cacheStore.Store(preparedStmtDBKey, preparedStmt)
db.ConnPool = preparedStmt
}
db.Statement = &Statement{
DB: db,
ConnPool: db.ConnPool,
Context: context.Background(),
Clauses: map[string]clause.Clause{},
}
if err == nil && !config.DisableAutomaticPing {
if pinger, ok := db.ConnPool.(interface{ Ping() error }); ok {
err = pinger.Ping()
}
}
if err != nil {
config.Logger.Error(context.Background(), "failed to initialize database, got error %v", err)
}
return
}
可以看到,clone的初始值为1,我们现在知道,当我们连接数据库,得到一个db实例的时候,clone默认为1;
继续,让我们看一下session函数,即db配置函数的源码的一部分:
func (db *DB) Session(config *Session) *DB {
var (
txConfig = *db.Config
tx = &DB{
Config: &txConfig,
Statement: db.Statement,
Error: db.Error,
clone: 1,
}
)
//
//
if !config.NewDB {
tx.clone = 2
}
//
return tx
}
我们看到,当config.newdb为false时,clone为2,否则为1;
我们知道了这么多 某某情况下clone值为多少,现在我们需要知道 clone值对应的处理逻辑是什么。
如此,我们不妨看一下db.where函数的源码,作为我们开始时展示的例子,他一定有用到clone这一属性:
//gorm/chainable_api.go
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {
tx = db.getInstance()
if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: conds})
}
return
}
我们从这里能看到,where传入的所有参数,都不是直接作用于db身上的,而是通过db.getinstance,得到一个变量,再对这个变量进行操作。
我们顺下去,看这个db.getinstance的源码:
//gorm.go
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),
SkipHooks: db.Statement.SkipHooks,
}
} else {
// with clone statement
tx.Statement = db.Statement.clone()
tx.Statement.DB = tx
}
return tx
}
return db
}
终于,我们在这里看到了clone的处理逻辑。
当clone小于或等于0,db.getinstance直接返回db本身;否则先生成一个与db共享一些量的tx,但是clone这个字段被设置为默认值,也就是0;
当clone等于1时,tx生成一个新的statement,独立于原来db的statement,这个新的statement的clauses(相当于筛选条件,比如where中的语句就被这一变量处理)为空,并不含有之前的内容;
当clone大于0且不为1时,tx的状态直接调用db.statement.clone()函数,并将db调整为tx;
以下为这个函数的源码的一部分:
func (stmt *Statement) clone() *Statement {
newStmt := &Statement{
TableExpr: stmt.TableExpr,
Table: stmt.Table,
Model: stmt.Model,
Unscoped: stmt.Unscoped,
Dest: stmt.Dest,
ReflectValue: stmt.ReflectValue,
Clauses: map[string]clause.Clause{},
Distinct: stmt.Distinct,
Selects: stmt.Selects,
Omits: stmt.Omits,
Preloads: map[string][]interface{}{},
ConnPool: stmt.ConnPool,
Schema: stmt.Schema,
Context: stmt.Context,
RaiseErrorOnNotFound: stmt.RaiseErrorOnNotFound,
SkipHooks: stmt.SkipHooks,
}
//
for k, c := range stmt.Clauses {
newStmt.Clauses[k] = c
}
//
return newStmt
}
我们可以看到,这个函数将之前的db的statement中的clause全部复制到了newStmt中,这相当于tx这个DB实例拥有之前所添加过的所有条件。这里,就是gorm官方文档中所说的 not reusable的根本所在。
现在,让我们回到一开始的那个例子
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// db is a new initialized `*gorm.DB`, which is safe to reuse
db.Where("name = ?", "jinzhu").Where("age = ?", 18).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18;
db.Where("name = ?", "jinzhu2").Where("age = ?", 20).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu2' AND age = 20;
db.Find(&users)
// SELECT * FROM users;
这段代码中,第一行db.clone默认为1;
第二行db.where生成并返回了一个新实例tx,where中的条件被加进来tx.statement中,然后又有一个where函数,相当于tx.where().Find(),逻辑和之前一样,最后相当于tx2.Find(),在tx2这个实例上查找符合条件的记录;
第三行,我们复用了db,db的statement中的条件不含有第二行所用过的,因为之前都被加进了tx,tx2中,和db的状态无关;
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// db is a new initialized *gorm.DB, which is safe to reuse
tx := db.Where("name = ?", "jinzhu")
tx.Where("age = ?", 18).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18
tx.Where("age = ?", 28).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18 AND age = 28;
这段代码中,tx为db.where返回的那一个新的DB实例,必然会带上name=“jinzhu”这一条件,而且注意,根据之前我们看到的getinstance函数的源码,这个tx的clone是默认值:0;也就是说,在日后通过这个tx调用getinstance时,由于clone为0,getinstance直接返回tx本身,这也就是为什么后面所有条件都会累加到tx自己的statement上。
那么,有什么解决办法吗?
显然是有的。
我们可以使用诸如Session,Debug等函数,这些函数内部的设置会使得clone的值符合我们的需求;
tx := db.Where("name = ?", "jinzhu").Session(&gorm.Session{})
tx := db.Where("name = ?", "jinzhu").WithContext(context.Background())
tx := db.Where("name = ?", "jinzhu").Debug()
以session函数为例,传入一个空的sesion结构体,其中的NewDB字段为默认值:false,按照我们之前的源码:
if !config.NewDB {
tx.clone = 2
}
这个tx的clone被设置成了2,那么其后使用where等函数时,调用的getinstance便不会直接返回tx本身,按照之前所看到的源码,应该是生成一个新的DB实例,且含有之前的状态中的条件(注意,这里含有的条件显然是tx的条件,而不是由tx上一个生成的新实例的条件,这些新实例是平行关系,互不影响)。
全文终。