1. フックメカニズム
Hooks
メカニズムは、データベース操作に特定の変更が発生する前後にカスタムロジックを追加する方法です。新しいノードを追加したり、ノード間のエッジを削除したり、複数のノードを削除するなど、データベーススキーマを変更する際に、Hooks
を使用してデータの検証、ログ記録、権限チェック、または任意のカスタム操作を実行することができます。これにより、データの整合性とビジネスルールへの準拠が確保されると同時に、開発者は元のビジネスロジックを変更せずに追加機能を実装できます。
2. フックの登録方法
2.1 グローバルフックとローカルフック
グローバルフック(ランタイムフック
)はグラフ内のすべての種類の操作に有効です。これは、ログ記録やモニタリングなどのアプリケーション全体にロジックを追加するために適しています。ローカルフック(スキーマフック
)は特定の種類のスキーマ内で定義され、そのスキーマのタイプに一致する変更操作にのみ適用されます。ローカルフックを使用することで、特定のノードタイプに関連するすべてのロジックをスキーマ定義内の一か所に集約することができます。
2.2 フックの登録手順
コードでフックを登録する際に通常以下の手順が含まれます:
- フック関数の定義。この関数は
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("ミューテーション前:Type=%s, Operation=%s\n", m.Type(), m.Op())
// ミューテーション操作を実行
v, err := next.Mutate(ctx, m)
// ミューテーション操作の後にログを出力
log.Printf("ミューテーション後:Type=%s, Operation=%s\n", m.Type(), m.Op())
return v, err
})
}
- クライアントにフックを登録します。グローバルフックの場合、クライアントの
Use
メソッドを使用して登録できます。ローカルフックの場合、スキーマ内でHooks
メソッドを使用して登録できます。
// グローバルフックを登録
client.Use(logHook)
// ユーザータイプにのみ適用されるローカルフックを登録
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)
})
})
- 複数のフックを連鎖させることができ、登録の順序で実行されます。
3. フックの実行順序
フックの実行順序は、クライアントに登録された順序によって決まります。たとえば、client.Use(f, g, h)
はミューテーション操作で f(g(h(...)))
の順序で実行されます。この例では、f
が最初に実行され、次に g
、最後に h
が実行されます。
重要な点として、ランタイムフック(Runtimeフック
)はスキーマフック(Schemaフック
)よりも優先されます。つまり、g
と h
がスキーマで定義されたフックであり、f
が client.Use(...)
を使用して登録されている場合、実行順序は f(g(h(...)))
となります。これにより、ログなどのグローバルなロジックが他のすべてのフックよりも先に実行されることが保証されます。
4. フックによる問題の対処方法
フックを使用してデータベース操作をカスタマイズする際、インポートサイクルの問題に遭遇することがあります。これは通常、スキーマフックを使用しようとする際に発生します。なぜならば、ent/schema
パッケージは ent
コアパッケージを導入する可能性があります。そのため、ent
コアパッケージも ent/schema
をインポートしようとすると、循環依存関係が形成されます。
循環依存の原因
循環依存は通常、スキーマ定義と生成されたエンティティコードの間の双方向依存関係から生じます。つまり、ent/schema
が ent
に依存している(ent
フレームワークで提供される型を使用する必要があるため)、一方、ent
によって生成されたコードも ent/schema
に依存する(その中で定義されたスキーマ情報にアクセスする必要があるため)ためです。
円環依存関係の解決
もし円環依存エラーに遭遇したら、以下の手順に従うことができます:
- まずは、
ent/schema
で使用されているすべてのフックをコメントアウトしてください。 - 次に、
ent/schema
で定義されたカスタムタイプを新しいパッケージに移動します。たとえば、ent/schema/schematype
というパッケージを作成できます。 -
go generate ./...
コマンドを実行して、ent
パッケージを更新し、新しいパッケージパスを指すようにし、スキーマ内のタイプ参照を更新します。例えば、schema.T
をschematype.T
に変更します。 - 以前にコメントアウトしたフック参照をコメント解除し、再度
go generate ./...
コマンドを実行してください。この時点では、コード生成はエラーなしで進行するはずです。
これらの手順に従うことで、Hooksのインポートによって引き起こされる円環依存の問題を解決し、スキーマのロジックとHooksの実装がスムーズに進行できるようになります。
5. フックヘルパー関数の使用
ent
フレームワークは、Hooks実行のタイミングを制御するのに役立つ一連のフックヘルパー関数を提供しています。以下は、よく使用されるフックヘルパー関数の例です:
// 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. トランザクションフック
トランザクションフックを使用すると、トランザクションがコミット(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("コミット前")
return next.Commit(ctx, tx)
})
})
// トランザクション内で一連の操作を実行する...
return nil
})
上記のコードは、トランザクション内でコミット前に実行されるトランザクションフックを登録する方法を示しています。このフックは、すべてのデータベース操作が実行された後で、実際のトランザクションがコミットされる前に呼び出されます。