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

フック

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

Mutation

Mutation操作とは、データベースを変更させる操作のことです。 たとえば、グラフに新しいノードを追加したり、2つのノード間のエッジを削除したり、複数のノードを削除したりします。

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

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

生成されたノードタイプはそれぞれ独自のMutationタイプを保持しています。 たとえば、すべてのUserビルダーは、同じ生成されたUserMutationオブジェクトを共有します。

また、すべてのビルダータイプは、汎用の 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=Card 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"

評価の順序

フックはクライアントに登録された順序で呼び出されます。 したがって、 client.Use(f, g, h)f(g(h(...)))と実行されます。

また、 ランタイムフックスキーマフック の前に呼び出されることに注意してください。 つまり、ghがスキーマに定義されていて、client.Use(...)を使ってfが登録されていた場合、f(g(h(...)))のように実行されます。

フックヘルパー

生成されるhookパッケージは、フックが 実行されるタイミングを制御するのに役立ついくつかのヘルパーを提供します。

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

トランザクションフック

フックはアクティブなトランザクションにも登録でき、 Tx.Commit または Tx.Rollback で実行されます。 詳細については、 transactions で確認してください

コード生成フック

entc パッケージは、コード生成フェーズにフック(ミドルウェア) のリストを追加するオプションを提供します。 詳細については、 codegen で確認してください