美文网首页
gorm hook使用中的问题及核心源码解读

gorm hook使用中的问题及核心源码解读

作者: 跑马溜溜的球 | 来源:发表于2021-05-31 15:59 被阅读0次

本文针对的是gorm V2版本。hook官方文档可以点击这里,本文旨在对官方文档作一些补充说明。

下文中所有的DB均指gorm.Open返回的DB对象。

DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})

1. hook作用的对象

hook只能定义在model上,不能定义在gorm.DB上。

假设我们有User表,对应model如下,则可以定义BeforeCreate hook,用于插入数据前的检查。

type User struct {
    ID int64
    Name string
    Age int32
    IsAdmin bool
    IsValid bool
    LoginTime time.Time
}

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
    if u.Age < 10 || u.Name == ""{
        return errors.New("invalid Age or Name")
    }

    return nil
}

但是如果多Model上有同样的逻辑(比如插入数据后记录日志),若使用hook实现,只能在每个Model上分别实现,或者考虑改用gorm的callback机制实现。

2. 可以定义哪些hook接口?

我们能定义的所有hook接口可以在gorm/callbacks/interface.go中查到

//gorm/callbacks/interface.go
type BeforeCreateInterface interface {
    BeforeCreate(*gorm.DB) error
}

type AfterCreateInterface interface {
    AfterCreate(*gorm.DB) error
}

type BeforeUpdateInterface interface {
    BeforeUpdate(*gorm.DB) error
}

type AfterUpdateInterface interface {
    AfterUpdate(*gorm.DB) error
}

type BeforeSaveInterface interface {
    BeforeSave(*gorm.DB) error
}

type AfterSaveInterface interface {
    AfterSave(*gorm.DB) error
}

type BeforeDeleteInterface interface {
    BeforeDelete(*gorm.DB) error
}

type AfterDeleteInterface interface {
    AfterDelete(*gorm.DB) error
}

type AfterFindInterface interface {
    AfterFind(*gorm.DB) error
}

3. 各hook接口在何时被调用?调用次数是怎样的

方法 调用hoook 触发次数
Save BeforeCreate/AfterCreate/BeforeSave/AfterSave 一次
Create BeforeCreate/AfterCreate/BeforeSave/AfterSave 数组形式插入触发多次,create from map方式不会触发
Update BeforeUpdate/AfterUpdate/BeforeSave/AfterSave 一次
Delete BeforeDelete/AfterDelete 一次
Find/First/Last/Take AfterFind 查出几条数据则触发几次

说明:

  • create from map的例子
    DB.Model(&User{}).Create(map[string]interface{}{
        "Name":"nathan",
        "Age":6,
    })
  • AfterFind只在Find时可能调多次,因为只有Find可能返回多条数据。
  • 在没查出数据时,AfterFind不会触发。
  • 注意BeforeSave,AfterSave在Create和Update时也会调用。这意味着,如果你同时定义了BeforeSave和BeforeCreate,那么在执行Create时,两者都会被触发。
  • Save方法的作用,源码中的注释是这样说的:Save update value in database, if the value doesn't have primary key, will insert it。无主键则插入,有主键则update。

4. hook中return error会怎样?

hook return error后果
BeforeUpdate/BeforeSave/BeforeCreate 停止之后的执行
AfterUpdate/AfterSave/AfterCreate/AfterDelete 使得之前的数据库写入操作回滚
AfterFind 继续执行

说明:

  • 停止之后的执行是指,方法本身和之后的After**都不会被调用。比如BeforeCreate若返回error,则Create和AfterCreate都不会调用。

5. 如何跳过hook?

    skipHookDB := DB.Session(&gorm.Session{
        //设置跳过hook
        SkipHooks:true,
    })

    skipHookDB.Where("age = ?", 12).Delete(&User{})

在现在的DB上定义一个不同配置的Session,用这个session来执行sql即可。

6. hook机制在源码中是如何实现的?

我们以Create为例,说明一下hook的实现方式。

gorm中对库表的操作,都是基于callback机制的(对于callback,稍后会专门写一篇来讲)。

6.1 主体流程

//gorm@v1.21.9/callbacks/callbacks.go

//默认回调方法
func RegisterDefaultCallbacks(db *gorm.DB, config *Config) {
    enableTransaction := func(db *gorm.DB) bool {
        return !db.SkipDefaultTransaction
    }

    //注册Create相关的回调
    createCallback := db.Callback().Create()
    //对Create注册transaction回调
    createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
    createCallback.Register("gorm:before_create", BeforeCreate)
    createCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(true))
    createCallback.Register("gorm:create", Create(config))
    createCallback.Register("gorm:save_after_associations", SaveAfterAssociations(true))
    createCallback.Register("gorm:after_create", AfterCreate)
    //对Create注册commit_or_rollback_transaction回调
    //这也是为什么Create方法会默认开启事务
    createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
    
    ... 
    ...
}

上述代码注册了mysql相关的默认回方法,这里我们只截取了Create方法相关的回调。可以看到

  • gorm:before_create将调用BeforeCreate函数
  • gorm:create将调用Create函数
  • gorm:after_create将调用的AfterCreate函数
  • 以上三个函数在gorm@v1.21.9/callbacks/create.go中定义

所以,对一次Create操作,其核心流程如下:

BeforeSaveInterface, BeforeCreateInterface等,即是我们自定义的hook方法。

6.2 代码解读

BeforeCreate

//gorm@v1.21.9/callbacks/create.go
func BeforeCreate(db *gorm.DB) {
    if db.Error == nil && db.Statement.Schema != nil &&
        //未设置跳过Hook
        !db.Statement.SkipHooks &&
        //定义了BeforeSave或BeforeCreate
        (db.Statement.Schema.BeforeSave || db.Statement.Schema.BeforeCreate) {
        callMethod(db, func(value interface{}, tx *gorm.DB) (called bool) {
            //定义了BeforeSave
            if db.Statement.Schema.BeforeSave {
                //value即是当前要插入的数据对象,在我们例子中是User
                //断言数据对象上是否实现了BeforeSaveInterface接口,即我们的hook
                if i, ok := value.(BeforeSaveInterface); ok {
                    called = true
                    //调用hook方法BeforeSave
                    //通过db.AddError将错误加入db.Error
                    db.AddError(i.BeforeSave(tx))
                }
            }

            //定义了BeforeCreate
            if db.Statement.Schema.BeforeCreate {
                if i, ok := value.(BeforeCreateInterface); ok {
                    called = true
                    //调用hook方法BeforeCreate
                    db.AddError(i.BeforeCreate(tx))
                }
            }
            return called
        })
    }
}

AfterCreate的逻辑类似。

Create

func Create(config *Config) func(db *gorm.DB) {
    ...
        return func(db *gorm.DB) {
            //无错误才执行create操作
            //这也是为什么BeforeCreate返回错误,Create就不会执行
            if db.Error == nil {
                ... ...
            }
        }
    }
}

CommitOrRollbackTransaction

//gorm@v1.21.9/callbacks/transaction.go
func CommitOrRollbackTransaction(db *gorm.DB) {
    if !db.Config.SkipDefaultTransaction {
        //只有注册了started_transaction才会执行
        //因为没有开始事务Commit和Rollback都没有意义
        if _, ok := db.InstanceGet("gorm:started_transaction"); ok {
            //db无Error提交
            if db.Error == nil {
                db.Commit()
            } else {
                //有Error回滚
                db.Rollback()
            }
            db.Statement.ConnPool = db.ConnPool
        }
    }
}

相关文章

  • gorm hook使用中的问题及核心源码解读

    本文针对的是gorm V2版本。hook官方文档可以点击这里[https://www.kancloud.cn/sl...

  • 01GORM源码解读

    简介 起步 数据库连接 gorm.DB 事务实现 总结 简介 GORM 源码解读, 基于 v1.9.11 版本. ...

  • hook delegate 正确姿势

    核心hook方法:网上有类似hook方式,但没考虑继承和子类的问题,会导致循环调用crash。 使用 案例:(XX...

  • 04GORM源码解读

    简介 查询查询流程构建查询 SQL 语句条件语句小结 search 结构体search 的定义search 的方法...

  • 03GORM源码解读

    简介 模型交互AutoMigratecreateTable callbacks实际注册流程createCallba...

  • 02GORM源码解读

    简介 定义模型 ModelStruct获取表名StructFieldRelationship更多 Scope模型解...

  • React hook 10种 Hook

    React hook 10种 Hook (详细介绍及使用) React Hook是什么?React官网是这么介绍的...

  • link

    Java 并发编程(一) 独占锁 synchronized 和 Lock 使用及源码解读设计模式Spring St...

  • Golang GORM使用

    Golang GORM使用 gorm gorm是go语言中实现数据库访问的ORM(对象关系映射)库。使用这个库,我...

  • Android Jetpack LiveData解析

    目前关于LiveData源码解读的文章非常多了,本文就不重复了,这里只对核心流程做解读。关于源码流程,推荐:And...

网友评论

      本文标题:gorm hook使用中的问题及核心源码解读

      本文链接:https://www.haomeiwen.com/subject/fpsbsltx.html