Interceptors

Interceptors are execution middleware for various types of Ent queries. Contrary to hooks, interceptors are applied on the read-path and implemented as interfaces, allows them to intercept and modify the query at different stages, providing more fine-grained control over queries’ behavior. For example, see the Traverser interface below.

Defining an Interceptor

To define an Interceptor, users can declare a struct that implements the Intercept method or use the predefined ent.InterceptFunc adapter.

  1. ent.InterceptFunc(func(next ent.Querier) ent.Querier {
  2. return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
  3. // Do something before the query execution.
  4. value, err := next.Query(ctx, query)
  5. // Do something after the query execution.
  6. return value, err
  7. })
  8. })

In the example above, the ent.Query represents a generated query builder (e.g., ent.<T>Query) and accessing its methods requires type assertion. For example:

  1. ent.InterceptFunc(func(next ent.Querier) ent.Querier {
  2. return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
  3. if q, ok := query.(*ent.UserQuery); ok {
  4. q.Where(user.Name("a8m"))
  5. }
  6. return next.Query(ctx, query)
  7. })
  8. })

However, the utilities generated by the intercept feature flag enable the creation of generic interceptors that can be applied to any query type. The intercept feature flag can be added to a project in one of two ways:

Configuration

  • CLI
  • Go

If you are using the default go generate config, add --feature intercept option to the ent/generate.go file as follows:

ent/generate.go

  1. package ent
  2. //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature intercept ./schema

It is recommended to add the schema/snapshot feature-flag along with the intercept flag to enhance the development experience, for example:

  1. //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature intercept,schema/snapshot ./schema

If you are using the configuration from the GraphQL documentation, add the feature flag as follows:

  1. // +build ignore
  2. package main
  3. import (
  4. "log"
  5. "entgo.io/ent/entc"
  6. "entgo.io/ent/entc/gen"
  7. )
  8. func main() {
  9. opts := []entc.Option{
  10. entc.FeatureNames("intercept"),
  11. }
  12. if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
  13. log.Fatalf("running ent codegen: %v", err)
  14. }
  15. }

It is recommended to add the schema/snapshot feature-flag along with the intercept flag to enhance the development experience, for example:

  1. opts := []entc.Option{
  2. - entc.FeatureNames("intercept"),
  3. + entc.FeatureNames("intercept", "schema/snapshot"),
  4. }

Interceptors Registration

Interceptors - 图1info

You should notice that similar to schema hooks, if you use the Interceptors option in your schema, you MUST add the following import in the main package, because a circular import is possible between the schema package and the generated ent package:

  1. import _ "<project>/ent/runtime"

Using the generated intercept package

Once the feature flag was added to your project, the creation of interceptors is possible using the intercept package:

  • intercept.Func
  • intercept.TraverseFunc
  • intercept.NewQuery
  1. client.Intercept(
  2. intercept.Func(func(ctx context.Context, q intercept.Query) error {
  3. // Limit all queries to 1000 records.
  4. q.Limit(1000)
  5. return nil
  6. })
  7. )
  1. client.Intercept(
  2. intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
  3. // Apply a predicate/filter to all queries.
  4. q.WhereP(predicate)
  5. return nil
  6. })
  7. )
  1. ent.InterceptFunc(func(next ent.Querier) ent.Querier {
  2. return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
  3. // Get a generic query from a typed-query.
  4. q, err := intercept.NewQuery(query)
  5. if err != nil {
  6. return nil, err
  7. }
  8. q.Limit(1000)
  9. return next.Intercept(ctx, query)
  10. })
  11. })

Defining a Traverser

In some cases, there is a need to intercept graph traversals and modify their builders before continuing to the nodes returned by the query. For example, in the query below, we want to ensure that only active users are traversed in any graph traversals in the system:

  1. intercept.TraverseUser(func(ctx context.Context, q *ent.UserQuery) error {
  2. q.Where(user.Active(true))
  3. return nil
  4. })

After defining and registering such Traverser, it will take effect on all graph traversals in the system. For example:

  1. func TestTypedTraverser(t *testing.T) {
  2. ctx := context.Background()
  3. client := enttest.Open(t, dialect.SQLite, "file:ent?mode=memory&_fk=1")
  4. defer client.Close()
  5. a8m, nat := client.User.Create().SetName("a8m").SaveX(ctx), client.User.Create().SetName("nati").SetActive(false).SaveX(ctx)
  6. client.Pet.CreateBulk(
  7. client.Pet.Create().SetName("a").SetOwner(a8m),
  8. client.Pet.Create().SetName("b").SetOwner(a8m),
  9. client.Pet.Create().SetName("c").SetOwner(nat),
  10. ).ExecX(ctx)
  11. // Get pets of all users.
  12. if n := client.User.Query().QueryPets().CountX(ctx); n != 3 {
  13. t.Errorf("got %d pets, want 3", n)
  14. }
  15. // Add an interceptor that filters out inactive users.
  16. client.User.Intercept(
  17. intercept.TraverseUser(func(ctx context.Context, q *ent.UserQuery) error {
  18. q.Where(user.Active(true))
  19. return nil
  20. }),
  21. )
  22. // Only pets of active users are returned.
  23. if n := client.User.Query().QueryPets().CountX(ctx); n != 2 {
  24. t.Errorf("got %d pets, want 2", n)
  25. }
  26. }

Interceptors vs. Traversers

Both Interceptors and Traversers can be used to modify the behavior of queries, but they do so at different stages the execution. Interceptors function as middleware and allow modifying the query before it is executed and modifying the records after they are returned from the database. For this reason, they are applied only in the final stage of the query - during the actual execution of the statement on the database. On the other hand, Traversers are called one stage earlier, at each step of a graph traversal allowing them to modify both intermediate and final queries before they are joined together.

In summary, a Traverse function is a better fit for adding default filters to graph traversals while using an Intercept function is better for implementing logging or caching capabilities to the application.

  1. client.User.Query().
  2. QueryGroups(). // User traverse functions applied.
  3. QueryPosts(). // Group traverse functions applied.
  4. All(ctx) // Post traverse and intercept functions applied.

Examples

Soft Delete

The soft delete pattern is a common use-case for interceptors and hooks. The example below demonstrates how to add such functionality to all schemas in the project using ent.Mixin:

  • Mixin
  • Mixin usage
  • Runtime usage
  1. // SoftDeleteMixin implements the soft delete pattern for schemas.
  2. type SoftDeleteMixin struct {
  3. mixin.Schema
  4. }
  5. // Fields of the SoftDeleteMixin.
  6. func (SoftDeleteMixin) Fields() []ent.Field {
  7. return []ent.Field{
  8. field.Time("delete_time").
  9. Optional(),
  10. }
  11. }
  12. type softDeleteKey struct{}
  13. // SkipSoftDelete returns a new context that skips the soft-delete interceptor/mutators.
  14. func SkipSoftDelete(parent context.Context) context.Context {
  15. return context.WithValue(parent, softDeleteKey{}, true)
  16. }
  17. // Interceptors of the SoftDeleteMixin.
  18. func (d SoftDeleteMixin) Interceptors() []ent.Interceptor {
  19. return []ent.Interceptor{
  20. intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
  21. // Skip soft-delete, means include soft-deleted entities.
  22. if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
  23. return nil
  24. }
  25. d.P(q)
  26. return nil
  27. }),
  28. }
  29. }
  30. // Hooks of the SoftDeleteMixin.
  31. func (d SoftDeleteMixin) Hooks() []ent.Hook {
  32. return []ent.Hook{
  33. hook.On(
  34. func(next ent.Mutator) ent.Mutator {
  35. return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
  36. // Skip soft-delete, means delete the entity permanently.
  37. if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
  38. return next.Mutate(ctx, m)
  39. }
  40. mx, ok := m.(interface {
  41. SetOp(ent.Op)
  42. Client() *gen.Client
  43. SetDeleteTime(time.Time)
  44. WhereP(...func(*sql.Selector))
  45. })
  46. if !ok {
  47. return nil, fmt.Errorf("unexpected mutation type %T", m)
  48. }
  49. d.P(mx)
  50. mx.SetOp(ent.OpUpdate)
  51. mx.SetDeleteTime(time.Now())
  52. return mx.Client().Mutate(ctx, m)
  53. })
  54. },
  55. ent.OpDeleteOne|ent.OpDelete,
  56. ),
  57. }
  58. }
  59. // P adds a storage-level predicate to the queries and mutations.
  60. func (d SoftDeleteMixin) P(w interface{ WhereP(...func(*sql.Selector)) }) {
  61. w.WhereP(
  62. sql.FieldIsNull(d.Fields()[0].Descriptor().Name),
  63. )
  64. }
  1. // Pet holds the schema definition for the Pet entity.
  2. type Pet struct {
  3. ent.Schema
  4. }
  5. // Mixin of the Pet.
  6. func (Pet) Mixin() []ent.Mixin {
  7. return []ent.Mixin{
  8. SoftDeleteMixin{},
  9. }
  10. }
  1. // Filter out soft-deleted entities.
  2. pets, err := client.Pet.Query().All(ctx)
  3. if err != nil {
  4. return err
  5. }
  6. // Include soft-deleted entities.
  7. pets, err := client.Pet.Query().All(schema.SkipSoftDelete(ctx))
  8. if err != nil {
  9. return err
  10. }

Limit number of records

The following example demonstrates how to limit the number of records returned from the database using an interceptor function:

  1. client.Intercept(
  2. intercept.Func(func(ctx context.Context, q intercept.Query) error {
  3. // LimitInterceptor limits the number of records returned from
  4. // the database to 1000, in case Limit was not explicitly set.
  5. if ent.QueryFromContext(ctx).Limit == nil {
  6. q.Limit(1000)
  7. }
  8. return nil
  9. }),
  10. )

Multi-project support

The example below demonstrates how to write a generic interceptor that can be used in multiple projects:

  • Definition
  • Usage
  1. // Project-level example. The usage of "entgo" package emphasizes that this interceptor does not rely on any generated code.
  2. func SharedLimiter[Q interface{ Limit(int) }](f func(entgo.Query) (Q, error), limit int) entgo.Interceptor {
  3. return entgo.InterceptFunc(func(next entgo.Querier) entgo.Querier {
  4. return entgo.QuerierFunc(func(ctx context.Context, query entgo.Query) (entgo.Value, error) {
  5. l, err := f(query)
  6. if err != nil {
  7. return nil, err
  8. }
  9. l.Limit(limit)
  10. // LimitInterceptor limits the number of records returned from the
  11. // database to the configured one, in case Limit was not explicitly set.
  12. if entgo.QueryFromContext(ctx).Limit == nil {
  13. l.Limit(limit)
  14. }
  15. return next.Query(ctx, query)
  16. })
  17. })
  18. }
  1. client1.Intercept(SharedLimiter(intercept1.NewQuery, limit))
  2. client2.Intercept(SharedLimiter(intercept2.NewQuery, limit))