Hooks
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
La opción Hooks permite añadir lógica personalizada antes y después de operaciones que modifican el grafo.
Mutación
Una operación de mutación es aquella que modifica la base de datos. Por ejemplo, añadir un nuevo nodo al grafo, eliminar una relación entre 2 nodos o borrar múltiples nodos.
Existen 5 tipos de mutaciones:
Create- Crear un nodo en el grafo.UpdateOne- Actualizar un nodo en el grafo. Por ejemplo, incrementar un campo.Update- Actualizar múltiples nodos en el grafo que cumplan un predicado.DeleteOne- Eliminar un nodo del grafo.Delete- Eliminar todos los nodos que cumplan un predicado.
Each generated node type has its own type of mutation. For example, all User builders, share
the same generated UserMutation object. However, all builder types implement the generic ent.Mutation interface.
A diferencia de los disparadores de base de datos, los hooks se ejecutan a nivel de aplicación, no a nivel de base de datos. Si necesitas ejecutar lógica específica a nivel de base de datos, utiliza disparadores como se explica en la guía de migración de esquema.
Hooks
Hooks are functions that get an ent.Mutator and return a mutator back.
They function as middleware between mutators. It's similar to the popular HTTP middleware pattern.
type (
// Mutator is the interface that wraps the Mutate method.
Mutator interface {
// Mutate apply the given mutation on the graph.
Mutate(context.Context, Mutation) (Value, error)
}
// Hook defines the "mutation middleware". A function that gets a Mutator
// and returns a Mutator. For example:
//
// 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
)
Existen 2 tipos de hooks de mutación: hooks de esquema y hooks de tiempo de ejecución. Los hooks de esquema se usan principalmente para definir lógica de mutación personalizada en el esquema, mientras que los hooks de tiempo de ejecución sirven para añadir elementos como registro de logs, métricas, trazas, etc. Veamos ambas versiones:
Hooks de tiempo de ejecución
Comencemos con un ejemplo breve que registra todas las operaciones de mutación de cualquier tipo:
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()
// Run the auto migration tool.
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// Add a global hook that runs on all types and all operations.
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=User Time=46.23µs ConcreteType=*ent.UserMutation
}
Los hooks globales son útiles para añadir trazas, métricas, logs y más. Pero a veces los usuarios necesitan mayor granularidad:
func main() {
// <client was defined in the previous block>
// Add a hook only on user mutations.
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)
})
})
// Add a hook only on update operations.
client.Use(hook.On(Logger(), ent.OpUpdate|ent.OpUpdateOne))
// Reject delete operations.
client.Use(hook.Reject(ent.OpDelete|ent.OpDeleteOne))
}
Supón que quieres compartir un hook que modifique un campo entre varios tipos (ej. Group y User).
Existen ~2 formas de hacerlo:
// Option 1: use type assertion.
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) {
// A schema with a "name" field must implement the NameSetter interface.
if ns, ok := m.(NameSetter); ok {
ns.SetName("Ariel Mashraki")
}
return next.Mutate(ctx, m)
})
})
// Option 2: use the generic ent.Mutation interface.
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 {
// An error is returned, if the field is not defined in
// the schema, or if the type mismatch the field type.
}
return next.Mutate(ctx, m)
})
})
Hooks de esquema
Los hooks de esquema se definen en el esquema de tipo y se aplican solo a mutaciones que coinciden con ese tipo de esquema. La motivación para definir hooks en el esquema es centralizar toda la lógica relacionada con el tipo de nodo en un único lugar: el esquema.
package schema
import (
"context"
"fmt"
gen "<project>/ent"
"<project>/ent/hook"
"entgo.io/ent"
)
// Card holds the schema definition for the CreditCard entity.
type Card struct {
ent.Schema
}
// Hooks of the Card.
func (Card) Hooks() []ent.Hook {
return []ent.Hook{
// First 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)
})
},
// Limit the hook only for these operations.
ent.OpCreate|ent.OpUpdate|ent.OpUpdateOne,
),
// Second hook.
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
})
},
}
}
Registro de hooks
Al usar hooks de esquema, puede darse una importación cíclica entre el paquete de esquema
y el paquete generado de ent. Para evitar este escenario, ent genera un paquete ent/runtime que se encarga
de registrar los schema-hooks en tiempo de ejecución.
Los usuarios DEBEN importar ent/runtime para registrar los hooks de esquema.
El paquete puede importarse en el paquete main (cerca de donde se importa el driver de base de datos),
o en el paquete que crea el ent.Client.
import _ "<project>/ent/runtime"
Error de ciclo de importación
En el primer intento de configurar hooks de esquema en tu proyecto, puedes encontrarte con un error como este:
entc/load: parse schema dir: import cycle not allowed: [ent/schema ent/hook ent/ ent/schema]
To resolve this issue, move the custom types used by the generated code to a separate package: "Type1", "Type2"
Este error puede ocurrir porque el código generado depende de tipos personalizados definidos en el paquete ent/schema, pero este
paquete también importa ent/hook. Esta importación indirecta del paquete ent crea un ciclo, causando el error.
Para resolverlo, sigue estas instrucciones:
Primero, comenta cualquier uso de hooks, políticas de privacidad o interceptores en
ent/schema.Mueve los tipos personalizados definidos en
ent/schemaa un nuevo paquete, por ejemploent/schema/schematype.Ejecuta
go generate ./...para actualizar el paquete generadoenty que apunte al nuevo paquete. Por ejemplo,schema.Tse convierte enschematype.T.Descomenta los hooks, políticas de privacidad o interceptores y ejecuta
go generate ./...nuevamente. La generación de código debería completarse sin errores.
Orden de evaluación
Los hooks se ejecutan en el orden en que se registraron en el cliente. Por lo tanto, client.Use(f, g, h)
ejecuta f(g(h(...))) sobre las mutaciones.
También ten en cuenta que los runtime hooks se ejecutan antes que los schema hooks. Es decir, si g
y h se definieron en el esquema, y f se registró usando client.Use(...),
se ejecutarán de la siguiente manera: f(g(h(...))).
Asistentes de hooks
El paquete de hooks generado proporciona varios asistentes que te ayudarán a controlar cuándo se ejecutará un 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{
// Execute "HookA" only for the UpdateOne and DeleteOne operations.
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne),
// Don't execute "HookB" on Create operation.
hook.Unless(HookB(), ent.OpCreate),
// Execute "HookC" only if the ent.Mutation is changing the "status" field,
// and clearing the "dirty" field.
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty"))),
// Disallow changing the "password" field on Update (many) operation.
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 de transacción
Los hooks también pueden registrarse en transacciones activas, y se ejecutarán en Tx.Commit o Tx.Rollback.
Para más información, consulta la página de transacciones.
Hooks de generación de código
El paquete entc proporciona una opción para añadir una lista de hooks (middlewares) a la fase de generación de código.
Para más información, consulta la página de generación de código.