1. Mecanismo de Hooks
O mecanismo de Hooks
é um método para adicionar lógica personalizada antes ou após alterações específicas ocorrerem nas operações do banco de dados. Ao modificar o esquema do banco de dados, como adicionar novos nós, excluir arestas entre nós ou excluir vários nós, podemos usar Hooks
para realizar validação de dados, registro de log, verificações de permissão ou operações personalizadas. Isso é crucial para garantir a consistência dos dados e a conformidade com as regras de negócios, ao mesmo tempo que permite que os desenvolvedores adicionem funcionalidades adicionais sem alterar a lógica de negócios original.
2. Método de Registro de Hook
2.1 Hooks Globais e Hooks Locais
Hooks globais (Hooks em tempo de execução
) são eficazes para todos os tipos de operações no gráfico. Eles são adequados para adicionar lógica a toda a aplicação, como registro e monitoramento. Hooks locais (Hooks de esquema
) são definidos dentro de esquemas de tipos específicos e se aplicam apenas a operações de mutação que correspondam ao tipo do esquema. Usar hooks locais permite centralizar toda a lógica relacionada a tipos específicos de nós em um só lugar, ou seja, na definição do esquema.
2.2 Etapas para Registrar Hooks
Registrar um hook no código geralmente envolve as seguintes etapas:
- Definir a função do hook. Esta função recebe um
ent.Mutator
e retorna ument.Mutator
. Por exemplo, criando um hook de registro simples:
logHook := func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// Imprimir registros antes da operação de mutação
log.Printf("Antes da mutação: Tipo=%s, Operação=%s\n", m.Type(), m.Op())
// Realizar a operação de mutação
v, err := next.Mutate(ctx, m)
// Imprimir registros após a operação de mutação
log.Printf("Após a mutação: Tipo=%s, Operação=%s\n", m.Type(), m.Op())
return v, err
})
}
- Registrar o hook com o cliente. Para hooks globais, você pode registrá-los usando o método
Use
do cliente. Para hooks locais, você pode registrá-los no esquema usando o métodoHooks
do tipo.
// Registrar um hook global
client.Use(logHook)
// Registrar um hook local, apenas aplicado ao tipo de Usuário
client.User.Use(func(next ent.Mutator) ent.Mutator {
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
// Adicionar lógica específica
// ...
return next.Mutate(ctx, m)
})
})
- Você pode encadear vários hooks, e eles serão executados na ordem de registro.
3. Ordem de Execução dos Hooks
A ordem de execução dos hooks é determinada pela ordem em que são registrados no cliente. Por exemplo, client.Use(f, g, h)
será executado na operação de mutação na ordem de f(g(h(...)))
. Neste exemplo, f
é o primeiro hook a ser executado, seguido por g
e, finalmente, h
.
É importante notar que hooks em tempo de execução (Hooks em tempo de execução
) têm precedência sobre hooks de esquema (Hooks de esquema
). Isso significa que se g
e h
forem hooks definidos no esquema, enquanto f
for registrado usando client.Use(...)
, a ordem de execução será f(g(h(...)))
. Isso garante que a lógica global, como o registro, seja executada antes de todos os outros hooks.
4. Lidando com Problemas Causados por Hooks
Ao personalizar operações de banco de dados usando Hooks, podemos encontrar o problema de ciclos de importação. Isso geralmente ocorre ao tentar usar hooks de esquema, pois o pacote ent/schema
pode introduzir o pacote principal ent
. Se o pacote principal ent
também tentar importar ent/schema
, será formada uma dependência circular.
Causas de Dependências Circulares
As dependências circulares geralmente surgem de dependências bidirecionais entre definições de esquema e código de entidade gerado. Isso significa que ent/schema
depende de ent
(porque precisa usar tipos fornecidos pelo framework ent
), enquanto o código gerado por ent
também depende de ent/schema
(porque precisa acessar as informações do esquema definidas dentro dele).
Resolução de Dependências Circulares
Se você encontrar um erro de dependência circular, pode seguir estes passos:
- Primeiro, comente todos os hooks usados em
ent/schema
. - Em seguida, mova os tipos personalizados definidos em
ent/schema
para um novo pacote, por exemplo, você pode criar um pacote chamadoent/schema/schematype
. - Execute o comando
go generate ./...
para atualizar o pacoteent
, de forma que aponte para o novo caminho do pacote, atualizando as referências de tipo no esquema. Por exemplo, altereschema.T
paraschematype.T
. - Descomente as referências de hooks previamente comentadas e execute o comando
go generate ./...
novamente. Neste ponto, a geração de código deve prosseguir sem erros.
Seguindo estes passos, podemos resolver o problema de dependência circular causado por importações de Hooks, garantindo que a lógica do esquema e a implementação dos Hooks possam prosseguir sem problemas.
5. Uso de Funções Auxiliares de Hook
O framework ent
fornece um conjunto de funções auxiliares de hook, que podem nos ajudar a controlar o momento da execução dos Hooks. Abaixo estão alguns exemplos de funções auxiliares de Hook comumente utilizadas:
// Execute apenas o HookA para operações UpdateOne e DeleteOne
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)
// Não execute o HookB durante a operação Create
hook.Unless(HookB(), ent.OpCreate)
// Execute o HookC apenas quando a Mutation estiver alterando o campo "status" e limpando o campo "dirty"
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))
// Proibir a alteração do campo "password" em operações de Update (várias)
hook.If(
hook.FixedError(errors.New("a senha não pode ser editada em atualizações múltiplas")),
hook.And(
hook.HasOp(ent.OpUpdate),
hook.Or(
hook.HasFields("password"),
hook.HasClearedFields("password"),
),
),
)
Essas funções auxiliares nos permitem controlar precisamente as condições de ativação dos Hooks para diferentes operações.
6. Transaction Hooks
Transaction Hooks permitem que Hooks específicos sejam executados quando uma transação é confirmada (Tx.Commit
) ou anulada (Tx.Rollback
). Isso é muito útil para garantir a consistência dos dados e a atomicidade das operações.
Exemplo de Transaction Hooks
client.Tx(ctx, func(tx *ent.Tx) error {
// Registrando um transaction hook - o hookBeforeCommit será executado antes do commit.
tx.OnCommit(func(next ent.Committer) ent.Committer {
return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
// A lógica antes do commit real pode ser colocada aqui.
fmt.Println("Antes do commit")
return next.Commit(ctx, tx)
})
})
// Realize uma série de operações dentro da transação...
return nil
})
O código acima mostra como registrar um transaction hook para ser executado antes de um commit em uma transação. Esse hook será chamado depois que todas as operações de banco de dados forem executadas e antes que a transação seja realmente confirmada.