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

我不禁感到疑惑,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 属性直接相关:
  3. clone = 0,获取当前 db 实例,即不做处理;
  4. clone = 1,返回一个新的 db,并清空查询条件;
  5. clone = 2,返回一个新的 db,不清空查询条件;
  6. 不管是那种克隆模式,都不会修改实例的连接池和上下文;

揭秘黑魔法

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

在调用 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优化掉也是理所当然,实际上的开发应当尽量不用。

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