Saltar al contenido principal

Interceptores

[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Los interceptores son middleware de ejecución para diversos tipos de consultas Ent. A diferencia de los hooks, los interceptores se aplican en la ruta de lectura y se implementan como interfaces, lo que les permite interceptar y modificar consultas en diferentes etapas, proporcionando un control más granular sobre el comportamiento de las consultas. Por ejemplo, consulta la interfaz Traverser más abajo.

Definición de un interceptor

Para definir un Interceptor, los usuarios pueden declarar una estructura que implemente el método Intercept o usar el adaptador predefinido ent.InterceptFunc.

ent.InterceptFunc(func(next ent.Querier) ent.Querier {
return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
// Do something before the query execution.
value, err := next.Query(ctx, query)
// Do something after the query execution.
return value, err
})
})

En el ejemplo anterior, ent.Query representa un constructor de consultas generado (ej. ent.<T>Query) y para acceder a sus métodos se requiere type assertion. Por ejemplo:

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

Sin embargo, las utilidades generadas por la feature flag intercept permiten crear interceptores genéricos aplicables a cualquier tipo de consulta. La feature flag intercept puede añadirse al proyecto de dos formas:

Configuración

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

ent/generate.go
package ent

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

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

Registro de interceptores

información

Debes notar que, similar a los hooks de esquema, si usas la opción Interceptors en tu esquema, DEBES agregar la siguiente importación en el paquete principal, para evitar posibles importaciones circulares entre el paquete de esquema y el paquete ent generado:

import _ "<project>/ent/runtime"

Usando el paquete generado intercept

Una vez añadida la feature flag a tu proyecto, es posible crear interceptores usando el paquete intercept:

client.Intercept(
intercept.Func(func(ctx context.Context, q intercept.Query) error {
// Limit all queries to 1000 records.
q.Limit(1000)
return nil
})
)

Definición de un traverser

En algunos casos es necesario interceptar recorridos de grafos y modificar sus constructores antes de continuar con los nodos devueltos por la consulta. Por ejemplo, en la siguiente consulta queremos asegurar que solo usuarios active sean recorridos en cualquier recorrido de grafo del sistema:

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

Tras definir y registrar este Traverser, afectará a todos los recorridos de grafos del sistema. Por ejemplo:

func TestTypedTraverser(t *testing.T) {
ctx := context.Background()
client := enttest.Open(t, dialect.SQLite, "file:ent?mode=memory&_fk=1")
defer client.Close()
a8m, nat := client.User.Create().SetName("a8m").SaveX(ctx), client.User.Create().SetName("nati").SetActive(false).SaveX(ctx)
client.Pet.CreateBulk(
client.Pet.Create().SetName("a").SetOwner(a8m),
client.Pet.Create().SetName("b").SetOwner(a8m),
client.Pet.Create().SetName("c").SetOwner(nat),
).ExecX(ctx)

// Get pets of all users.
if n := client.User.Query().QueryPets().CountX(ctx); n != 3 {
t.Errorf("got %d pets, want 3", n)
}

// Add an interceptor that filters out inactive users.
client.User.Intercept(
intercept.TraverseUser(func(ctx context.Context, q *ent.UserQuery) error {
q.Where(user.Active(true))
return nil
}),
)

// Only pets of active users are returned.
if n := client.User.Query().QueryPets().CountX(ctx); n != 2 {
t.Errorf("got %d pets, want 2", n)
}
}

Interceptores vs. Traversers

Tanto Interceptors como Traversers pueden modificar el comportamiento de consultas, pero operan en etapas diferentes de la ejecución. Los interceptores funcionan como middleware que permite modificar la consulta antes de ejecutarla y los registros después de devolverse de la base de datos. Por esta razón, se aplican solo en la etapa final de la consulta - durante la ejecución real en la base de datos. Los traversers, en cambio, se llaman una etapa antes, en cada paso del recorrido del grafo, permitiendo modificar tanto consultas intermedias como finales antes de unirse.

En resumen, Traverse es más adecuado para añadir filtros por defecto en recorridos de grafos, mientras que Intercept es mejor para implementar capacidades de logging o caching.

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

Ejemplos

Borrado lógico

El borrado lógico es un caso de uso común para interceptores y hooks. El siguiente ejemplo demuestra cómo añadir esta funcionalidad a todos los esquemas usando ent.Mixin:

// SoftDeleteMixin implements the soft delete pattern for schemas.
type SoftDeleteMixin struct {
mixin.Schema
}

// Fields of the SoftDeleteMixin.
func (SoftDeleteMixin) Fields() []ent.Field {
return []ent.Field{
field.Time("delete_time").
Optional(),
}
}

type softDeleteKey struct{}

// SkipSoftDelete returns a new context that skips the soft-delete interceptor/mutators.
func SkipSoftDelete(parent context.Context) context.Context {
return context.WithValue(parent, softDeleteKey{}, true)
}

// Interceptors of the SoftDeleteMixin.
func (d SoftDeleteMixin) Interceptors() []ent.Interceptor {
return []ent.Interceptor{
intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
// Skip soft-delete, means include soft-deleted entities.
if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
return nil
}
d.P(q)
return nil
}),
}
}

// Hooks of the SoftDeleteMixin.
func (d SoftDeleteMixin) Hooks() []ent.Hook {
return []ent.Hook{
hook.On(
func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// Skip soft-delete, means delete the entity permanently.
if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
return next.Mutate(ctx, m)
}
mx, ok := m.(interface {
SetOp(ent.Op)
Client() *gen.Client
SetDeleteTime(time.Time)
WhereP(...func(*sql.Selector))
})
if !ok {
return nil, fmt.Errorf("unexpected mutation type %T", m)
}
d.P(mx)
mx.SetOp(ent.OpUpdate)
mx.SetDeleteTime(time.Now())
return mx.Client().Mutate(ctx, m)
})
},
ent.OpDeleteOne|ent.OpDelete,
),
}
}

// P adds a storage-level predicate to the queries and mutations.
func (d SoftDeleteMixin) P(w interface{ WhereP(...func(*sql.Selector)) }) {
w.WhereP(
sql.FieldIsNull(d.Fields()[0].Descriptor().Name),
)
}

Limitar registros

El siguiente ejemplo demuestra cómo limitar el número de registros devueltos desde la base de datos usando una función interceptor:

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

Soporte multi-proyecto

El siguiente ejemplo demuestra cómo escribir un interceptor genérico reusable en múltiples proyectos:

// Project-level example. The usage of "entgo" package emphasizes that this interceptor does not rely on any generated code.
func SharedLimiter[Q interface{ Limit(int) }](f func(entgo.Query) (Q, error), limit int) entgo.Interceptor {
return entgo.InterceptFunc(func(next entgo.Querier) entgo.Querier {
return entgo.QuerierFunc(func(ctx context.Context, query entgo.Query) (entgo.Value, error) {
l, err := f(query)
if err != nil {
return nil, err
}
l.Limit(limit)
// LimitInterceptor limits the number of records returned from the
// database to the configured one, in case Limit was not explicitly set.
if entgo.QueryFromContext(ctx).Limit == nil {
l.Limit(limit)
}
return next.Query(ctx, query)
})
})
}