1. Meccanismo dei Hooks

Il meccanismo dei Hooks è un metodo per aggiungere logiche personalizzate prima o dopo che avvengano specifici cambiamenti nelle operazioni del database. Quando si modifica lo schema del database, ad esempio aggiungendo nuovi nodi, cancellando collegamenti tra nodi o eliminando più nodi, possiamo utilizzare i Hooks per effettuare la convalida dei dati, la registrazione dei log, controlli di autorizzazione o qualsiasi operazione personalizzata. Questo è fondamentale per garantire la coerenza dei dati e il rispetto delle regole aziendali, consentendo anche ai programmatori di aggiungere funzionalità aggiuntive senza modificare la logica aziendale originale.

2. Metodo di Registrazione dei Hooks

2.1 Hooks Globali e Hooks Locali

I hooks globali (Hooks runtime) sono efficaci per tutti i tipi di operazioni nel grafo. Sono adatti per aggiungere logiche all'intera applicazione, come ad esempio logging e monitoraggio. I hooks locali (Hooks schema) sono definiti all'interno degli schemi di tipo specifico e si applicano solo alle operazioni di mutazione corrispondenti al tipo dello schema. L'utilizzo di hooks locali consente di centralizzare tutte le logiche relative a tipi di nodi specifici in un unico punto, ovvero nella definizione dello schema.

2.2 Passaggi per Registrare i Hooks

Solitamente la registrazione di un hook nel codice comporta i seguenti passaggi:

  1. Definire la funzione hook. Questa funzione prende un ent.Mutator e restituisce un ent.Mutator. Per esempio, creando un semplice hook di logging:
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // Stampare i log prima dell'operazione di mutazione
        log.Printf("Prima della mutazione: Tipo=%s, Operazione=%s\n", m.Type(), m.Op())
        // Eseguire l'operazione di mutazione
        v, err := next.Mutate(ctx, m)
        // Stampare i log dopo l'operazione di mutazione
        log.Printf("Dopo la mutazione: Tipo=%s, Operazione=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. Registrare l'hook con il client. Per i hooks globali, è possibile registrarli utilizzando il metodo Use del client. Per i hooks locali, è possibile registrarli nello schema utilizzando il metodo Hooks del tipo.
// Registrare un hook globale
client.Use(logHook)

// Registrare un hook locale, applicabile solo al tipo User
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // Aggiungere logica specifica
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. È possibile concatenare più hooks e saranno eseguiti nell'ordine della registrazione.

3. Ordine di Esecuzione dei Hooks

L'ordine di esecuzione dei hooks è determinato dall'ordine in cui vengono registrati con il client. Ad esempio, client.Use(f, g, h) verrà eseguito sull'operazione di mutazione nell'ordine f(g(h(...))). In questo esempio, f è il primo hook ad essere eseguito, seguito da g, e infine h.

È importante notare che i hooks runtime (Hooks runtime) hanno la precedenza sui hooks schema (Hooks schema). Ciò significa che se g e h sono hooks definiti nello schema mentre f è registrato utilizzando client.Use(...), l'ordine di esecuzione sarà f(g(h(...))). Ciò garantisce che le logiche globali, come il logging, vengano eseguite prima di tutti gli altri hooks.

4. Gestione dei Problemi Causati dai Hooks

Nel personalizzare le operazioni del database utilizzando i Hooks, potremmo incontrare il problema dei cicli di importazione. Ciò si verifica di solito quando si cercano di utilizzare gli hooks di schema, poiché il package ent/schema può introdurre il package ent core. Se il package ent core cerca anche di importare ent/schema, si verifica una dipendenza circolare.

Cause delle Dipendenze Circolari

Le dipendenze circolari sorgono di solito dalle dipendenze bidirezionali tra le definizioni degli schemi e il codice delle entità generate. Ciò significa che ent/schema dipende da ent (perché ha bisogno di utilizzare i tipi forniti dal framework ent), mentre il codice generato da ent dipende anche da ent/schema (perché ha bisogno di accedere alle informazioni dello schema definite al suo interno).

Risoluzione delle Dipendenze Circolari

Se incontri un errore di dipendenza circolare, puoi seguire questi passaggi:

  1. Per prima cosa, commenta tutti gli hooks utilizzati in ent/schema.
  2. Successivamente, sposta i tipi personalizzati definiti in ent/schema in un nuovo package, ad esempio puoi creare un package chiamato ent/schema/schematype.
  3. Esegui il comando go generate ./... per aggiornare il package ent, in modo che punti al nuovo percorso del package, aggiornando i riferimenti ai tipi nello schema. Ad esempio, cambia schema.T in schematype.T.
  4. Rimuovi i commenti dalle precedenti referenze agli hooks e esegui nuovamente il comando go generate ./.... A questo punto, la generazione del codice dovrebbe procedere senza errori.

Seguendo questi passaggi, possiamo risolvere il problema della dipendenza circolare causato dagli import degli Hooks, garantendo che la logica dello schema e l'implementazione degli Hooks possano procedere senza intoppi.

5. Utilizzo delle Funzioni Helper degli Hook

Il framework ent fornisce un set di funzioni helper degli hook, che possono aiutarci a controllare il momento dell'esecuzione degli Hooks. Di seguito ci sono alcuni esempi di funzioni helper degli hook comunemente utilizzate:

// Esegui solo l'HookA per le operazioni UpdateOne e DeleteOne
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

// Non eseguire l'HookB durante l'operazione di Creazione
hook.Unless(HookB(), ent.OpCreate)

// Esegui l'HookC solo quando la Mutation sta modificando il campo "status" e cancellando il campo "dirty"
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))

// Vietare la modifica del campo "password" nelle operazioni di aggiornamento multiplo
hook.If(
    hook.FixedError(errors.New("la password non può essere modificata in un aggiornamento multiplo")),
    hook.And(
        hook.HasOp(ent.OpUpdate),
        hook.Or(
            hook.HasFields("password"),
            hook.HasClearedFields("password"),
        ),
    ),
)

Queste funzioni helper ci consentono di controllare in modo preciso le condizioni di attivazione degli Hooks per diverse operazioni.

6. Transaction Hooks

I Transaction Hooks consentono di eseguire specifici Hooks quando una transazione viene confermata (Tx.Commit) o annullata (Tx.Rollback). Questo è molto utile per garantire la coerenza dei dati e l'atomicità delle operazioni.

Esempio di Transaction Hooks

client.Tx(ctx, func(tx *ent.Tx) error {
    // Registrazione di un transaction hook - l'hookBeforeCommit verrà eseguito prima del commit.
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // La logica prima del commit effettivo può essere inserita qui.
            fmt.Println("Prima del commit")
            return next.Commit(ctx, tx)
        })
    })

    // Esegui una serie di operazioni all'interno della transazione...

    return nil
})

Il codice sopra mostra come registrare un transaction hook da eseguire prima di un commit in una transazione. Questo hook verrà chiamato dopo che tutte le operazioni sul database sono eseguite e prima che la transazione venga effettivamente confermata.