gorm源码之db的克隆

1,141 阅读7分钟

gorm源码之db的克隆

前置知识简介:链式调用与方法

gorm中,方法是用来进行具体处理逻辑的函数。

gorm中的方法有三种,Chain MethodFinisher MethodNew Session Method.

  1. chain method可以用来将特定筛选条件增加到数据库的状态中;

    常见的有db.where,db.select等

  2. finisher method可以立即执行回调,生成并执行sql语句。

    比如create,first,find等

  3. 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上一个生成的新实例的条件,这些新实例是平行关系,互不影响)。

全文终。