メインコンテンツへスキップする

フック

Hooks オプションによって、グラフを変更する操作の前後にカスタムロジックを追加できます

Mutation

A mutation operation is an operation that mutates the database. たとえば、グラフに新しいノードを追加したり、2つのノード間のエッジを削除したり、複数のノードを削除したりします。

Mutationには5つの種類があります

  • Create - グラフにノードを作成します
  • UpdateOne - グラフのノードを更新します。 たとえば、フィールドを追加します
  • Update - 述語に一致するグラフ内の複数のノードを更新します
  • DeleteOne - グラフからノードを削除します
  • Delete - 述語に一致するすべてのノードを削除します

生成されたノードタイプはそれぞれ独自のMutationタイプを保持しています。 For example, all User builders, share the same generated UserMutation object.

また、すべてのビルダータイプは、汎用の ent.Mutation インターフェースを実装しています。

Hooks

Hooksとは、ent.Mutatorを取得して、Mutatorを返す関数です。 HookはMutator間のミドルウェアとして機能します。 これはよく知られているHTTPのミドルウェアパターンに似ています。

type (
// Mutator は Mutate メソッドをラップするインターフェイスです。
Mutator interface {
// Mutate はグラフに与えられたMutationを適用します。
Mutate(context.Context, Mutation) (Value, error)
}

// Hookは「mutionmiddleware」を定義しています。 この関数は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
)

Mutationフックには、スキーマ・フックランタイム・フックの2種類があります。 スキーマフックは主にスキーマにカスタムな変更ロジックを定義するために使われ、ランタイムフックはロギング、メトリックス、トレーシングなどを追加するために使われます。 2つのバージョンを見てみましょう:

ランタイムフック

すべての型のすべての変更操作を記録する短い例から始めましょう。

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)
// 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>

// ユーザーの変更のみのフックを追加
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)
})
})

// 更新操作のみのフックを追加
client.Use(hook.On(Logger(), ent.OpUpdate|ent.OpUpdateOne))

// 削除操作を拒否
client.Use(hook.Reject(ent.OpDelete|ent.OpDeleteOne))
}

複数のタイプ間でフィールドを変更するフックを共有すると仮定します(例: GroupUser)。 2つの方法があります:

// 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は、CreditCardエンティティのスキーマ定義を保持しています。
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)
// Post mutation action.
fmt.Println("new value:", v)
return v, err
})
},
}
}

フックの登録

スキーマフックを使用する場合、schemaパッケージと生成されたentパッケージの間で循環的なインポートが行われる可能性があります。 このシナリオを回避するために、ent は実行時にスキーマフックを登録するent/runtime パッケージを生成します。

ユーザーは、スキーマフックを登録するために、ent/runtime をインポートしなければなりません。 このパッケージは、mainパッケージ(データベースドライバがインポートされる場所の近く) 、またはent.Clientを作成するパッケージでインポートできます。
import _ "<project>/ent/runtime"

Import Cycle Error

At the first attempt to set up schema hooks in your project, you may encounter an error like the following:

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"

The error may occur because the generated code relies on custom types defined in the ent/schema package, but this package also imports the ent/hook package. This indirect import of the ent package creates a loop, causing the error to occur. To resolve this issue, follow these instructions:

  • First, comment out any usage of hooks, privacy policy, or interceptors from the ent/schema.
  • Move the custom types defined in the ent/schema to a new package, for example, ent/schema/schematype.
  • Run go generate ./... to update the generated ent package to point to the new package. For example, schema.T becomes schematype.T.
  • Uncomment the hooks, privacy policy, or interceptors, and run go generate ./... again. The code generation should now pass without error.

評価の順序

Hooks are called in the order they were registered to the client. Thus, client.Use(f, g, h) executes f(g(h(...))) on mutations.

Also note, that runtime hooks are called before schema hooks. That is, if g, and h were defined in the schema, and f was registered using client.Use(...), they will be executed as follows: f(g(h(...))).

フックヘルパー

The generated hooks package provides several helpers that can help you control when a hook will be executed.

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"),
),
),
),
}
}

トランザクションフック

Hooks can also be registered on active transactions, and will be executed on Tx.Commit or Tx.Rollback. For more information, read about it in the transactions page.

コード生成フック

The entc package provides an option to add a list of hooks (middlewares) to the code-generation phase. For more information, read about it in the codegen page.