跳到主要内容

钩子(Hooks)

Hooks 选项允许在图突变操作之前或之后添加自定义逻辑。

突变(Mutation)

突变操作是修改数据库的操作。例如,向图中新增节点、移除两个节点之间的边或删除多个节点。

有5种突变类型:

  • Create - 在图中创建节点
  • UpdateOne - 更新图中的一个节点。例如增加节点字段。
  • Update - 更新图中满足条件的多个节点。
  • DeleteOne - 删除图中的一个节点。
  • Delete - 删除满足条件的所有节点。

每个生成的节点都有自己的突变类型。例如,所有的 User 构建器,共享相同的生成后的 UserMutation 对象。因此所有的构建器类型都实现通用的 ent.Mutation 接口。

数据库触发器支持

与数据库触发器不同,钩子执行在应用层面而不是数据库层面。如果你需要在数据库层面执行特定逻辑,使用 模式迁移指南 中所述的数据库触发器。

钩子(Hooks)

钩子是一个可以获得 ent.Mutator 并返回突变器的函数。他们在突变器之间作为中间件发挥作用。与常用的 HTTP 中间件相似。

type (
// Mutator is the interface that wraps the Mutate method.
Mutator interface {
// Mutate apply the given mutation on the graph.
Mutate(context.Context, Mutation) (Value, error)
}

// Hook defines the "mutation middleware". A function that gets a Mutator
// and returns a Mutator. For example:
//
// 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 hooksruntime hooksschema hooks 主要用于在模式中定义自定义突变逻辑,runtime 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()
// Run the auto migration tool.
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// Add a global hook that runs on all types and all operations.
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)
// Output:
// 2020/03/21 10:59:10 Op=Create Type=User Time=46.23µs ConcreteType=*ent.UserMutation
}

全局钩子对于添加追踪、度量、日志等更有效。但有时用户希望更细粒度:

func main() {
// <client was defined in the previous block>

// Add a hook only on user mutations.
client.User.Use(func(next ent.Mutator) ent.Mutator {
// Use the "<project>/ent/hook" to get the concrete type of the mutation.
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
return next.Mutate(ctx, m)
})
})

// Add a hook only on update operations.
client.Use(hook.On(Logger(), ent.OpUpdate|ent.OpUpdateOne))

// Reject delete operations.
client.Use(hook.Reject(ent.OpDelete|ent.OpDeleteOne))
}

假设你希望在多个类型(例如 GroupUser)间共享钩子来更改字段,有两种方法可以实现:

// Option 1: use type assertion.
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) {
// A schema with a "name" field must implement the NameSetter interface.
if ns, ok := m.(NameSetter); ok {
ns.SetName("Ariel Mashraki")
}
return next.Mutate(ctx, m)
})
})

// Option 2: use the generic ent.Mutation interface.
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 {
// An error is returned, if the field is not defined in
// the schema, or if the type mismatch the field type.
}
return next.Mutate(ctx, m)
})
})

模式钩子Schema hooks

模式钩子定义在类型模式中,仅应用于与模式类型相匹配的突变操作。 在模式中定义钩子的初衷是将所有与节点类型相关的逻辑集中于一处——即模式本身。

package schema

import (
"context"
"fmt"

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

"entgo.io/ent"
)

// Card holds the schema definition for the CreditCard entity.
type Card struct {
ent.Schema
}

// Hooks of the Card.
func (Card) Hooks() []ent.Hook {
return []ent.Hook{
// First 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)
})
},
// Limit the hook only for these operations.
ent.OpCreate|ent.OpUpdate|ent.OpUpdateOne,
),
// Second hook.
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)
// Post mutation action.
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]
To resolve this issue, move the custom types used by the generated code to a separate package: "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{
// Execute "HookA" only for the UpdateOne and DeleteOne operations.
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne),

// Don't execute "HookB" on Create operation.
hook.Unless(HookB(), ent.OpCreate),

// Execute "HookC" only if the ent.Mutation is changing the "status" field,
// and clearing the "dirty" field.
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty"))),

// Disallow changing the "password" field on Update (many) operation.
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 包提供在代码生成阶段添加一系列钩子(中间件)的选项。更多信息参阅 代码生成