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

Posted ball球

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gorm hook使用中的问题及核心源码解读相关的知识,希望对你有一定的参考价值。

本文针对的是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触发次数
SaveBeforeCreate/AfterCreate/BeforeSave/AfterSave一次
CreateBeforeCreate/AfterCreate/BeforeSave/AfterSave数组形式插入触发多次,create from map方式不会触发
UpdateBeforeUpdate/AfterUpdate/BeforeSave/AfterSave一次
DeleteBeforeDelete/AfterDelete一次
Find/First/Last/TakeAfterFind查出几条数据则触发几次

说明:

  • 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会怎样?

hookreturn 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操作,其核心流程如下:

image

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使用中的问题及核心源码解读的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot 核心源码解读

Spring源码解读之核心容器下节

唯一插件化Replugin源码及原理深度剖析--唯一Hook点原理

唯一插件化Replugin源码及原理深度剖析--唯一Hook点原理

Koa源码解读

Derek解读Bytom源码-protobuf生成比原核心代码