一架梯子,一头程序猿,仰望星空!
Go Ent ORM框架教程 > 内容正文

Hooks机制


1. Hooks机制

Hooks机制是在数据库操作发生特定变更之前或之后加入自定义逻辑的一种方法。在修改数据库图谱时,例如添加新节点,删除节点间的边或删除多个节点,我们可以通过Hooks来进行数据验证、日志记录、权限检查或任何自定义操作。这对于确保数据一致性和符合业务规则至关重要,同时也允许开发者在不改变原有业务逻辑的基础上,增加额外功能。

2. Hook注册方法

2.1 全局钩子与局部钩子

全局钩子(Runtime hooks)对图中所有类型的所有操作都有效。它们适用于需要对整个应用程序添加逻辑,如日志记录和监控等。局部钩子(Schema hooks)则定义在特定的类型模式中,并且只适用于匹配该模式类型的变更操作。使用局部钩子允许将与特定节点类型相关的所有逻辑集中在一个地方,即模式定义中。

2.2 Hooks注册步骤

在代码中注册一个Hook通常以下列步骤进行:

  1. 定义Hook函数。这个函数接受一个ent.Mutator并返回一个ent.Mutator。例如,创建一个简单的日志记录钩子:
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // 在变更操作之前打印日志
        log.Printf("Before mutating: Type=%s, Operation=%s\n", m.Type(), m.Op())
        // 执行变更操作
        v, err := next.Mutate(ctx, m)
        // 在变更操作之后打印日志
        log.Printf("After mutating: Type=%s, Operation=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. 注册钩子到客户端。如果是全局钩子,可以通过使用客户端的Use方法来注册。如果是局部钩子,可以通过类型的Hooks方法在模式(Schema)里注册。
// 注册全局钩子
client.Use(logHook)

// 注册局部钩子,只应用在User类型上
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // 添加特定的逻辑
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. 可以链式注册多个钩子,他们会按照注册顺序执行。

3. Hooks执行顺序

钩子的执行顺序是根据它们注册到客户端的顺序决定的。例如,client.Use(f, g, h)在变更操作上执行的顺序是f(g(h(...)))。在这个例子中,f是第一个执行的钩子,然后是g,最后是h

需要注意的是,运行时钩子(Runtime hooks)优先于模式钩子(Schema hooks)执行。也就是说,如果gh是在模式中定义的钩子而f是使用client.Use(...)注册的,那么执行的顺序将是f(g(h(...)))。这可以保证例如日志记录之类的全局逻辑在所有其他钩子之前执行。

4. 处理Hooks带来的问题

当我们在使用Hooks进行数据库操作的定制时,可能会遇到循环依赖(import cycle)的问题。这通常发生在尝试使用schema hooks时,因为ent/schema包可能会引入了ent主包,此时如果ent主包也尝试去引入ent/schema,就形成了循环依赖。

导致循环依赖的原因

循环依赖产生的原因通常是因为schema定义和实体(entity)生成代码的双向依赖。也就是说,ent/schema既依赖于ent(因为它需要使用ent框架提供的类型),同时,ent生成的代码也会依赖于ent/schema(因为它需要访问您定义在其中的schema信息)。

解决循环依赖的方法

如果您遇到循环依赖错误,您可以按照下述步骤操作:

  1. 首先,您需要注释掉所有在ent/schema中使用的hooks。
  2. 接下来,将ent/schema中定义的自定义类型移动到一个新的包中,例如可以创建一个名为ent/schema/schematype的包。
  3. 运行go generate ./...命令来更新ent包,使其指向新的包路径,以便schema中的类型引用更新为例如:将schema.T更改为schematype.T
  4. 解开之前注释掉的hooks引用,并再次执行go generate ./...命令。这时,代码的生成应该能够通过,而不再出现错误。

通过以上步骤,我们就可以解决因为Hooks导入而引起的循环依赖问题,确保schema的逻辑和Hooks的实现能够顺利进行。

5. Hook帮助函数的使用

ent框架生成了一套钩子帮助函数(hook helpers),这些函数能够帮助我们控制Hooks的执行时机。下面是一些常用的Hook帮助函数的使用例子:

// 仅对UpdateOne和DeleteOne操作执行HookA
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

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

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

// 禁止在Update(多个)操作中更改"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"),
        ),
    ),
)

这些帮助函数使得我们可以精准地控制不同操作下Hooks的激活条件。

6. 事务钩子

事务钩子(Transaction Hooks)允许在事务提交(Tx.Commit)或回滚(Tx.Rollback)时执行特定的Hooks。这在确保数据一致性和执行操作的原子性方面非常有用。

事务钩子的例子

client.Tx(ctx, func(tx *ent.Tx) error {
    // 注册事务钩子 - 这里的hookBeforeCommit将在提交前执行。
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // 在实际提交之前的逻辑可以放在这里。
            fmt.Println("Before commit")
            return next.Commit(ctx, tx)
        })
    })

    // 进行事务中的一系列操作...
    
    return nil
})

上述代码展示了如何在事务中注册一个在提交前运行的事务钩子。这个钩子会在所有数据库操作执行之后和事务实际提交之前被调用。