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

我不禁感到疑惑,gormRowsAffected 在进行查询,如果查到数据,也是有值的,为什么在这里可以用 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
}

这个函数很简单,

  1. clone = 0 时,不做处理,返回 db
  2. clone > 0clone = 1 时,返回一个新的 dbclone变成了0),statement 中,连接池和上下文延用之前的,把条件和变量置为空;
  3. clone > 0clone > 1(目前只有clone为2)时,返回一个新的 dbclone变成了0),statement 完全复制之前的db

db.getInstance()的小结:

  1. 如其名,这个函数的作用是获取一个 db 实例;
  2. 获取的 db 跟原 dbclone 属性直接相关:
    1. clone = 0,获取当前 db 实例,即不做处理;
    2. clone = 1,返回一个新的 db,并清空查询条件;
    3. clone = 2,返回一个新的 db,不清空查询条件;
  3. 不管是那种克隆模式,都不会修改实例的连接池和上下文;

揭秘黑魔法

看到这里,这个“黑魔法”的原理已经呈现在我们眼前:

在调用 FirstOrCreate 函数的时候,如果此时 clone 不为0,则会在调用 Limit 函数的时候生成一个新的实例tx

txdb 是两个不同的实例, Find 查到的数据不为空时,txRowsAffected 会变化,而 dbRowsAffected 仍然不变。

因此在查到数据且不进行 Update 的情况下,函数会直接返回db,而RowsAffected 为 0。

基于上述理论,大胆猜想 Session 这个函数必定会改变 dbclone 属性,查看 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优化掉也是理所当然,实际上的开发应当尽量不用。

“黑魔法”应当少用,但是值得探究,这可比生啃源码有趣得多。

posted @ 2022-09-27 16:06  weirwei  阅读(697)  评论(0)    收藏  举报