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 | 触发次数 |
---|---|---|
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使用中的问题及核心源码解读的主要内容,如果未能解决你的问题,请参考以下文章
唯一插件化Replugin源码及原理深度剖析--唯一Hook点原理