1. Mecanismo de Hooks

El mecanismo de Hooks es un método para agregar lógica personalizada antes o después de ocurrencias específicas en operaciones de base de datos. Al modificar el esquema de la base de datos, como agregar nuevos nodos, eliminar conexiones entre nodos o borrar múltiples nodos, podemos usar los Hooks para realizar validación de datos, registro de logs, comprobación de permisos u operaciones personalizadas. Esto es crucial para garantizar la consistencia de los datos y el cumplimiento de las reglas comerciales, al tiempo que permite a los desarrolladores agregar funcionalidades adicionales sin alterar la lógica empresarial original.

2. Método de Registro de Hooks

2.1 Hooks Globales y Hooks Locales

Los hooks globales (Hooks en tiempo de ejecución) son efectivos para todos los tipos de operaciones en el gráfico. Son adecuados para agregar lógica a toda la aplicación, como registro y monitoreo. Los hooks locales (Hooks de esquema) se definen dentro de esquemas de tipos específicos y solo se aplican a operaciones de mutación que coinciden con el tipo del esquema. El uso de hooks locales permite que toda la lógica relacionada con tipos de nodos específicos esté centralizada en un solo lugar, es decir, en la definición del esquema.

2.2 Pasos para Registrar Hooks

El registro de un hook en el código generalmente implica los siguientes pasos:

  1. Definir la función del hook. Esta función toma un ent.Mutator y devuelve un ent.Mutator. Por ejemplo, creando un hook de registro simple:
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // Imprimir logs antes de la operación de mutación
        log.Printf("Antes de mutar: Tipo=%s, Operación=%s\n", m.Type(), m.Op())
        // Realizar la operación de mutación
        v, err := next.Mutate(ctx, m)
        // Imprimir logs después de la operación de mutación
        log.Printf("Después de mutar: Tipo=%s, Operación=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. Registrar el hook con el cliente. Para hooks globales, puedes registrarlos usando el método Use del cliente. Para hooks locales, puedes registrarlos en el esquema usando el método Hooks del tipo.
// Registrar un hook global
client.Use(logHook)

// Registrar un hook local, solo aplicado al tipo de Usuario
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // Agregar lógica específica
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. Puedes encadenar múltiples hooks, y se ejecutarán en el orden de registro.

3. Orden de Ejecución de Hooks

El orden de ejecución de los hooks está determinado por el orden en que se registran con el cliente. Por ejemplo, client.Use(f, g, h) se ejecutará en la operación de mutación en el orden de f(g(h(...))). En este ejemplo, f es el primer hook que se ejecuta, seguido por g y finalmente h.

Es importante tener en cuenta que los hooks en tiempo de ejecución (Hooks en tiempo de ejecución) tienen precedencia sobre los hooks de esquema (Hooks de esquema). Esto significa que si g y h son hooks definidos en el esquema mientras f está registrado usando client.Use(...), el orden de ejecución será f(g(h(...))). Esto asegura que la lógica global, como el registro, se ejecute antes que todos los demás hooks.

4. Manejo de Problemas Causados por Hooks

Al personalizar operaciones de base de datos utilizando Hooks, es posible que nos encontremos con el problema de ciclos de importación. Esto suele ocurrir al intentar utilizar hooks de esquema, ya que el paquete ent/schema puede introducir el paquete ent core. Si el paquete ent core también intenta importar ent/schema, se forma una dependencia circular.

Causas de las Dependencias Circulares

Las dependencias circulares suelen surgir de dependencias bidireccionales entre definiciones de esquemas y código de entidades generado. Esto significa que ent/schema depende de ent (porque necesita usar tipos proporcionados por el marco ent), mientras que el código generado por ent también depende de ent/schema (porque necesita acceder a la información del esquema definida dentro de él).

Resolución de Dependencias Circulares

Si te encuentras con un error de dependencia circular, puedes seguir estos pasos:

  1. En primer lugar, comenta todos los hooks utilizados en ent/schema.
  2. Luego, traslada los tipos personalizados definidos en ent/schema a un nuevo paquete, por ejemplo, puedes crear un paquete llamado ent/schema/schematype.
  3. Ejecuta el comando go generate ./... para actualizar el paquete ent, de manera que apunte a la nueva ruta del paquete, actualizando las referencias de tipos en el esquema. Por ejemplo, cambia schema.T por schematype.T.
  4. Descomenta las referencias a los hooks previamente comentados y vuelve a ejecutar el comando go generate ./.... En este punto, la generación de código debería proceder sin errores.

Siguiendo estos pasos, podemos resolver el problema de la dependencia circular causada por las importaciones de Hooks, asegurando que la lógica del esquema y la implementación de Hooks puedan proseguir sin problemas.

5. Uso de Funciones Auxiliares de Hooks

El framework ent proporciona un conjunto de funciones auxiliares de hooks, las cuales nos pueden ayudar a controlar el momento de ejecución de los Hooks. A continuación se presentan algunos ejemplos de funciones auxiliares de Hooks comúnmente utilizadas:

// Ejecutar solo HookA para operaciones UpdateOne y DeleteOne
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

// No ejecutar HookB durante la operación de Crear
hook.Unless(HookB(), ent.OpCreate)

// Ejecutar HookC solo cuando la Mutación esté modificando el campo "status" y borrando el campo "dirty"
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))

// Prohibir modificar el campo "password" en operaciones de Actualización (múltiples)
hook.If(
    hook.FixedError(errors.New("no se puede editar la contraseña en múltiples actualizaciones")),
    hook.And(
        hook.HasOp(ent.OpUpdate),
        hook.Or(
            hook.HasFields("password"),
            hook.HasClearedFields("password"),
        ),
    ),
)

Estas funciones auxiliares nos permiten controlar de manera precisa las condiciones de activación de los Hooks para diferentes operaciones.

6. Hooks de Transacción

Los Hooks de Transacción permiten que Hooks específicos se ejecuten cuando una transacción se confirma (Tx.Commit) o se revierte (Tx.Rollback). Esto es muy útil para garantizar la consistencia de los datos y la atomicidad de las operaciones.

Ejemplo de Hooks de Transacción

client.Tx(ctx, func(tx *ent.Tx) error {
    // Registrando un hook de transacción: el hookBeforeCommit se ejecutará antes de la confirmación.
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // Aquí se puede colocar la lógica antes de la confirmación real.
            fmt.Println("Antes de la confirmación")
            return next.Commit(ctx, tx)
        })
    })

    // Realizar una serie de operaciones dentro de la transacción...

    return nil
})

El código anterior muestra cómo registrar un hook de transacción para ejecutarse antes de una confirmación en una transacción. Este hook se llamará después de que se ejecuten todas las operaciones de la base de datos y antes de que la transacción se confirme realmente.