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:
- Definire la funzione hook. Questa funzione prende un
ent.Mutator
e restituisce unent.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
})
}
- 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 metodoHooks
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)
})
})
- È 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:
- Per prima cosa, commenta tutti gli hooks utilizzati in
ent/schema
. - Successivamente, sposta i tipi personalizzati definiti in
ent/schema
in un nuovo package, ad esempio puoi creare un package chiamatoent/schema/schematype
. - Esegui il comando
go generate ./...
per aggiornare il packageent
, in modo che punti al nuovo percorso del package, aggiornando i riferimenti ai tipi nello schema. Ad esempio, cambiaschema.T
inschematype.T
. - 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.