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:

  1. Definir a função do hook. Esta função recebe um ent.Mutator e retorna um ent.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
    })
}
  1. 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étodo Hooks 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)
    })
})
  1. 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:

  1. Primeiro, comente todos os hooks usados em ent/schema.
  2. Em seguida, mova os tipos personalizados definidos em ent/schema para um novo pacote, por exemplo, você pode criar um pacote chamado ent/schema/schematype.
  3. Execute o comando go generate ./... para atualizar o pacote ent, de forma que aponte para o novo caminho do pacote, atualizando as referências de tipo no esquema. Por exemplo, altere schema.T para schematype.T.
  4. 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.