1. Mécanisme des Hooks

Le mécanisme des Hooks est une méthode permettant d'ajouter une logique personnalisée avant ou après des changements spécifiques intervenant dans les opérations de la base de données. Lors de la modification du schéma de la base de données, telle que l'ajout de nouveaux nœuds, la suppression de liens entre les nœuds ou la suppression de plusieurs nœuds, nous pouvons utiliser les Hooks pour effectuer une validation des données, enregistrer des journaux, vérifier les autorisations ou effectuer des opérations personnalisées. Ceci est crucial pour garantir la cohérence des données et la conformité aux règles métier, tout en permettant aux développeurs d'ajouter des fonctionnalités supplémentaires sans altérer la logique métier initiale.

2. Méthode d'enregistrement des Hooks

2.1 Hooks Globaux et Hooks Locaux

Les hooks globaux (Hooks d'exécution) sont effectifs pour tous les types d'opérations dans le graphe. Ils sont adaptés pour ajouter une logique à l'ensemble de l'application, tels que la journalisation et la surveillance. Les hooks locaux (Hooks de schéma) sont définis dans des schémas de types spécifiques et s'appliquent uniquement aux opérations de mutation correspondant au type du schéma. L'utilisation de hooks locaux permet de centraliser toute la logique liée à des types de nœuds spécifiques, à savoir dans la définition du schéma.

2.2 Étapes d'enregistrement des Hooks

L'enregistrement d'un hook dans le code implique généralement les étapes suivantes :

  1. Définir la fonction de hook. Cette fonction prend un ent.Mutator en entrée et renvoie un ent.Mutator. Par exemple, créer un hook simple de journalisation :
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // Afficher des journaux avant l'opération de mutation
        log.Printf("Avant mutation : Type=%s, Opération=%s\n", m.Type(), m.Op())
        // Effectuer l'opération de mutation
        v, err := next.Mutate(ctx, m)
        // Afficher des journaux après l'opération de mutation
        log.Printf("Après mutation : Type=%s, Opération=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. Enregistrer le hook avec le client. Pour les hooks globaux, vous pouvez les enregistrer en utilisant la méthode Use du client. Pour les hooks locaux, vous pouvez les enregistrer dans le schéma en utilisant la méthode Hooks du type.
// Enregistrer un hook global
client.Use(logHook)

// Enregistrer un hook local, appliqué uniquement au type Utilisateur
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // Ajouter une logique spécifique
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. Vous pouvez chaîner plusieurs hooks, et ils seront exécutés dans l'ordre d'enregistrement.

3. Ordre d'exécution des Hooks

L'ordre d'exécution des hooks est déterminé par l'ordre dans lequel ils sont enregistrés avec le client. Par exemple, client.Use(f, g, h) s'exécutera sur l'opération de mutation dans l'ordre de f(g(h(...))). Dans cet exemple, f est le premier hook à être exécuté, suivi de g, et enfin h.

Il est important de noter que les hooks d'exécution (Hooks d'exécution) prévalent sur les hooks de schéma (Hooks de schéma). Cela signifie que si g et h sont des hooks définis dans le schéma tandis que f est enregistré en utilisant client.Use(...), l'ordre d'exécution sera f(g(h(...))). Cela garantit que la logique globale, telle que la journalisation, est exécutée avant tous les autres hooks.

4. Traitement des Problèmes Provoqués par les Hooks

Lors de la personnalisation des opérations de la base de données à l'aide des Hooks, nous pouvons rencontrer le problème des cycles d'importation. Cela se produit généralement lors de la tentative d'utilisation de hooks de schéma, car le package ent/schema peut introduire le package de base ent. Si le package de base ent tente également d'importer ent/schema, une dépendance circulaire est formée.

Causes des Dépendances Circulaires

Les dépendances circulaires surviennent généralement des dépendances bidirectionnelles entre les définitions de schéma et le code d'entité généré. Cela signifie que ent/schema dépend de ent (car il a besoin d'utiliser les types fournis par le framework ent), tandis que le code généré par ent dépend également de ent/schema (car il a besoin d'accéder aux informations de schéma qui y sont définies).

Résolution des Dépendances Circulaires

Si vous rencontrez une erreur de dépendance circulaire, vous pouvez suivre ces étapes :

  1. Tout d'abord, commentez tous les hooks utilisés dans ent/schema.
  2. Ensuite, déplacez les types personnalisés définis dans ent/schema vers un nouveau package. Par exemple, vous pouvez créer un package nommé ent/schema/schematype.
  3. Exécutez la commande go generate ./... pour mettre à jour le package ent, afin qu'il pointe vers le nouveau chemin du package, mettant à jour les références de types dans le schéma. Par exemple, remplacez schema.T par schematype.T.
  4. Décommentez les références de hooks précédemment commentées et exécutez à nouveau la commande go generate ./.... À ce stade, la génération de code devrait se dérouler sans erreurs.

En suivant ces étapes, nous pouvons résoudre le problème de dépendance circulaire causé par les importations de Hooks, en veillant à ce que la logique du schéma et la mise en œuvre des Hooks puissent se dérouler en douceur.

5. Utilisation des Fonctions d'Aide pour les Hooks

Le framework ent fournit un ensemble de fonctions d'aide pour les hooks, qui peuvent nous aider à contrôler le moment de l'exécution des Hooks. Voici quelques exemples de fonctions d'aide Hook couramment utilisées :

// Exécuter uniquement HookA pour les opérations UpdateOne et DeleteOne
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

// Ne pas exécuter HookB lors de l'opération de création
hook.Unless(HookB(), ent.OpCreate)

// Exécuter HookC uniquement lorsque la Mutation modifie le champ "status" et efface le champ "dirty"
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))

// Interdire la modification du champ "password" dans les opérations de mise à jour (multiples)
hook.If(
    hook.FixedError(errors.New("le mot de passe ne peut pas être modifié lors de la mise à jour multiple")),
    hook.And(
        hook.HasOp(ent.OpUpdate),
        hook.Or(
            hook.HasFields("password"),
            hook.HasClearedFields("password"),
        ),
    ),
)

Ces fonctions d'aide nous permettent de contrôler précisément les conditions d'activation des Hooks pour différentes opérations.

6. Hooks de Transaction

Les Hooks de Transaction permettent d'exécuter des Hooks spécifiques lorsqu'une transaction est validée (Tx.Commit) ou annulée (Tx.Rollback). Cela est très utile pour garantir la cohérence des données et l'atomicité des opérations.

Exemple de Hooks de Transaction

client.Tx(ctx, func(tx *ent.Tx) error {
    // Enregistrer un hook de transaction - le hookBeforeCommit sera exécuté avant la validation.
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // La logique avant la validation réelle peut être placée ici.
            fmt.Println("Avant validation")
            return next.Commit(ctx, tx)
        })
    })

    // Effectuer une série d'opérations dans la transaction...

    return nil
})

Le code ci-dessus montre comment enregistrer un hook de transaction pour s'exécuter avant une validation dans une transaction. Ce hook sera appelé après l'exécution de toutes les opérations de la base de données et avant que la transaction ne soit réellement validée.