Interceptores
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
- CLI
- Go
If you are using the default go generate config, add --feature intercept option to the ent/generate.go file as follows:
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
If you are using the configuration from the GraphQL documentation, add the feature flag as follows:
// +build ignore
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
)
func main() {
opts := []entc.Option{
entc.FeatureNames("intercept"),
}
if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
It is recommended to add the schema/snapshot feature-flag along with the
intercept flag to enhance the development experience, for example:
opts := []entc.Option{
- entc.FeatureNames("intercept"),
+ entc.FeatureNames("intercept", "schema/snapshot"),
}
Registro de interceptores
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:
- intercept.Func
- intercept.TraverseFunc
- intercept.NewQuery
client.Intercept(
intercept.Func(func(ctx context.Context, q intercept.Query) error {
// Limit all queries to 1000 records.
q.Limit(1000)
return nil
})
)
client.Intercept(
intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
// Apply a predicate/filter to all queries.
q.WhereP(predicate)
return nil
})
)
ent.InterceptFunc(func(next ent.Querier) ent.Querier {
return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
// Get a generic query from a typed-query.
q, err := intercept.NewQuery(query)
if err != nil {
return nil, err
}
q.Limit(1000)
return next.Intercept(ctx, query)
})
})
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:
- Mixin
- Mixin usage
- Runtime usage
// 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),
)
}
// Pet holds the schema definition for the Pet entity.
type Pet struct {
ent.Schema
}
// Mixin of the Pet.
func (Pet) Mixin() []ent.Mixin {
return []ent.Mixin{
SoftDeleteMixin{},
}
}
// Filter out soft-deleted entities.
pets, err := client.Pet.Query().All(ctx)
if err != nil {
return err
}
// Include soft-deleted entities.
pets, err := client.Pet.Query().All(schema.SkipSoftDelete(ctx))
if err != nil {
return err
}
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:
- Definition
- Usage
// 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)
})
})
}
client1.Intercept(SharedLimiter(intercept1.NewQuery, limit))
client2.Intercept(SharedLimiter(intercept2.NewQuery, limit))