跳到主要内容

Hooks

Hooks 选项允许在图结构发生变异的操作前后添加自定义逻辑。

变异操作 (Mutation)

变异操作是指会改变数据库状态的运算。例如:向图中添加新节点、移除两个节点之间的边,或删除多个节点。

共有 5 种变异类型:

  • Create - 在图中创建节点
  • UpdateOne - 更新图中的某个节点(例如递增其字段值)
  • Update - 更新图中所有匹配谓词的节点
  • DeleteOne - 从图中删除某个节点
  • Delete - 删除所有匹配谓词的节点

每个生成的节点类型都有其特定的变异类型。例如,所有 User 构建器共享同一个生成的 UserMutation 对象。但所有构建器类型都实现了通用的 ent.Mutation 接口。

对数据库触发器的支持

与数据库触发器不同,钩子是在应用层而非数据库层执行的。如需在数据库层执行特定逻辑,请按照模式迁移指南中的说明使用数据库触发器。

钩子 (Hooks)

钩子是接收一个 ent.Mutator 并返回一个变异器的函数。它们作为变异器之间的中间件,类似于常见的 HTTP 中间件模式。

type (
// Mutator 是封装了 Mutate 方法的接口
Mutator interface {
// Mutate 对图结构应用给定的变异操作
Mutate(context.Context, Mutation) (Value, error)
}

// Hook 定义了"变异中间件":接收 Mutator 并返回 Mutator 的函数
// 例如:
//
// hook := func(next ent.Mutator) ent.Mutator {
// return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// fmt.Printf("Type: %s, Operation: %s, ConcreteType: %T\n", m.Type(), m.Op(), m)
// return next.Mutate(ctx, m)
// })
// }
//
Hook func(Mutator) Mutator
)

存在两种类型的变异钩子——模式钩子 (schema hooks)运行时钩子 (runtime hooks)
模式钩子主要用于在模式中定义自定义变异逻辑,
运行时钩子则用于添加日志记录、指标收集、追踪等功能。
下面分别介绍这两种类型:

运行时钩子

首先看一个简短示例,它记录所有类型的所有变异操作:

func main() {
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer client.Close()
ctx := context.Background()
// 运行自动迁移工具
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// 添加一个全局钩子,作用于所有类型的所有操作
client.Use(func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
start := time.Now()
defer func() {
log.Printf("Op=%s\tType=%s\tTime=%s\tConcreteType=%T\n", m.Op(), m.Type(), time.Since(start), m)
}()
return next.Mutate(ctx, m)
})
})
client.User.Create().SetName("a8m").SaveX(ctx)
// 输出:
// 2020/03/21 10:59:10 Op=Create Type=User Time=46.23µs ConcreteType=*ent.UserMutation
}

全局钩子适用于添加追踪、指标、日志等功能。但有时需要更细粒度的控制:

func main() {
// <客户端定义与前面代码块相同>

// 仅对用户变异添加钩子
client.User.Use(func(next ent.Mutator) ent.Mutator {
// 使用 "<project>/ent/hook" 获取变异的具体类型
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
return next.Mutate(ctx, m)
})
})

// 仅对更新操作添加钩子
client.Use(hook.On(Logger(), ent.OpUpdate|ent.OpUpdateOne))

// 拒绝删除操作
client.Use(hook.Reject(ent.OpDelete|ent.OpDeleteOne))
}

假设需要在多个类型(如 GroupUser)间共享一个修改字段的钩子,有两种实现方式:

// 选项1:使用类型断言
client.Use(func(next ent.Mutator) ent.Mutator {
type NameSetter interface {
SetName(value string)
}
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// 包含"name"字段的模式必须实现 NameSetter 接口
if ns, ok := m.(NameSetter); ok {
ns.SetName("Ariel Mashraki")
}
return next.Mutate(ctx, m)
})
})

// 选项2:使用通用的 ent.Mutation 接口
client.Use(func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
if err := m.SetField("name", "Ariel Mashraki"); err != nil {
// 如果字段未在模式中定义,或类型与字段类型不匹配,则返回错误
}
return next.Mutate(ctx, m)
})
})

模式钩子

模式钩子定义在类型模式中,仅应用于匹配模式类型的变异操作。在模式中定义钩子的动机是将节点类型的所有相关逻辑集中在一个地方(即模式中)。

package schema

import (
"context"
"fmt"

gen "<project>/ent"
"<project>/ent/hook"

"entgo.io/ent"
)

// Card 定义信用卡实体的模式
type Card struct {
ent.Schema
}

// Card 的钩子
func (Card) Hooks() []ent.Hook {
return []ent.Hook{
// 第一个钩子
hook.On(
func(next ent.Mutator) ent.Mutator {
return hook.CardFunc(func(ctx context.Context, m *gen.CardMutation) (ent.Value, error) {
if num, ok := m.Number(); ok && len(num) < 10 {
return nil, fmt.Errorf("card number is too short")
}
return next.Mutate(ctx, m)
})
},
// 仅限这些操作生效
ent.OpCreate|ent.OpUpdate|ent.OpUpdateOne,
),
// 第二个钩子
func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
if s, ok := m.(interface{ SetName(string) }); ok {
s.SetName("Boring")
}
v, err := next.Mutate(ctx, m)
// 变异后操作
fmt.Println("new value:", v)
return v, err
})
},
}
}

钩子注册

使用模式钩子时,模式包与生成的 ent 包之间可能存在循环导入。为避免这种情况,ent 会生成一个 ent/runtime 包,负责在运行时注册模式钩子。

信息

用户必须导入 ent/runtime 才能注册模式钩子。 该包可在 main 包中导入(靠近数据库驱动导入位置),也可在创建 ent.Client 的包中导入。

import _ "<project>/ent/runtime"

循环导入错误

首次在项目中设置模式钩子时,可能会遇到如下错误:

entc/load: parse schema dir: import cycle not allowed: [ent/schema ent/hook ent/ ent/schema]
解决此问题的方法是:将生成代码所使用的自定义类型移至单独包中:"Type1", "Type2"

该错误的发生是因为生成的代码依赖于 ent/schema 包中定义的自定义类型,但该包又导入了 ent/hook 包。这种对 ent 包的间接导入形成循环,导致错误发生。请按以下步骤解决:

  • 首先注释掉 ent/schema 中所有钩子、隐私策略或拦截器的使用
  • ent/schema 中定义的自定义类型移至新包(例如 ent/schema/schematype
  • 运行 go generate ./... 更新生成的 ent 包以指向新包(例如 schema.T 变为 schematype.T
  • 取消注释钩子、隐私策略或拦截器,再次运行 go generate ./...,此时代码生成应能通过

执行顺序

钩子按照其注册到客户端的顺序被调用。因此 client.Use(f, g, h) 会按 f(g(h(...))) 的顺序执行变异操作。

请注意,运行时钩子会在模式钩子之前调用。即如果 gh 在模式中定义,而 f 通过 client.Use(...) 注册,它们的执行顺序将是:f(g(h(...)))

钩子辅助函数

生成的钩子包提供了多个辅助函数,可帮助控制钩子的执行时机。

package schema

import (
"context"
"fmt"

"<project>/ent/hook"

"entgo.io/ent"
"entgo.io/ent/schema/mixin"
)


type SomeMixin struct {
mixin.Schema
}

func (SomeMixin) Hooks() []ent.Hook {
return []ent.Hook{
// 仅在 UpdateOne 和 DeleteOne 操作时执行 "HookA"
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne),

// 在 Create 操作时不执行 "HookB"
hook.Unless(HookB(), ent.OpCreate),

// 仅当 ent.Mutation 正在更改 "status" 字段且清除 "dirty" 字段时执行 "HookC"
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty"))),

// 禁止在 Update (many) 操作时更改 "password" 字段
hook.If(
hook.FixedError(errors.New("password cannot be edited on update many")),
hook.And(
hook.HasOp(ent.OpUpdate),
hook.Or(
hook.HasFields("password"),
hook.HasClearedFields("password"),
),
),
),
}
}

事务钩子

钩子也可以在活动事务上注册,并会在 Tx.CommitTx.Rollback 时执行。更多信息请参阅事务页面

代码生成钩子

entc 包提供了在代码生成阶段添加钩子(中间件)列表的选项。更多信息请参阅代码生成页面