1. Механизм хуков

Механизм Hooks - это способ добавления пользовательской логики до или после конкретных изменений, происходящих в операциях с базой данных. При изменении схемы базы данных, таких как добавление новых узлов, удаление связей между узлами или удаление нескольких узлов, мы можем использовать Hooks для выполнения проверки данных, записи журнала, проверки разрешений или любых пользовательских операций. Это крайне важно для обеспечения согласованности данных и соответствия бизнес-правилам, а также позволяет разработчикам добавлять дополнительные функции, не изменяя исходную бизнес-логику.

2. Метод регистрации хуков

2.1 Глобальные и локальные хуки

Глобальные хуки (Runtime hooks) эффективны для всех типов операций в графе. Они подходят для добавления логики во всем приложении, такой как журналирование и мониторинг. Локальные хуки (Schema hooks) определяются в пределах конкретных схем типов и применяются только к мутационным операциям, соответствующим типу схемы. Использование локальных хуков позволяет централизовать всю логику, связанную с конкретными типами узлов, в одном месте, а именно в определении схемы.

2.2 Шаги регистрации хуков

Регистрация хука в коде обычно включает в себя следующие шаги:

  1. Определение функции хука. Эта функция принимает ent.Mutator и возвращает ent.Mutator. Например, создание простого логирующего хука:
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // Печать журнальных записей перед операцией мутации
        log.Printf("Перед мутацией: Тип=%s, Операция=%s\n", m.Type(), m.Op())
        // Выполнение операции мутации
        v, err := next.Mutate(ctx, m)
        // Печать журнальных записей после операции мутации
        log.Printf("После мутации: Тип=%s, Операция=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. Регистрация хука с клиентом. Для глобальных хуков их можно зарегистрировать с помощью метода Use клиента. Для локальных хуков их можно зарегистрировать в схеме с помощью метода Hooks типа.
// Зарегистрировать глобальный хук
client.Use(logHook)

// Зарегистрировать локальный хук, применяемый только к типу User
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // Добавление конкретной логики
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. Можно объединить несколько хуков, и они будут выполняться в порядке регистрации.

3. Порядок выполнения хуков

Порядок выполнения хуков определяется порядком их регистрации с клиентом. Например, client.Use(f, g, h) будет выполнять операцию мутации в порядке f(g(h(...))). В этом примере сначала будет выполнен хук f, затем g, и, наконец, h.

Важно отметить, что глобальные хуки (Runtime hooks) имеют приоритет над схемными хуками (Schema hooks). Это означает, что если g и h - это хуки, определенные в схеме, в то время как f зарегистрирован с использованием client.Use(...), порядок выполнения будет f(g(h(...))). Это гарантирует, что глобальная логика, такая как журналирование, будет выполнена перед всеми другими хуками.

4. Решение проблем, вызванных хуками

При настройке операций с базой данных с помощью Хуков мы можем столкнуться с проблемой циклических зависимостей. Это обычно происходит при попытке использования схемных хуков, так как пакет ent/schema может включать в себя ядро пакета ent. Если ядро ent также пытается импортировать ent/schema, формируется циклическая зависимость.

Причины циклических зависимостей

Циклические зависимости обычно возникают из-за двунаправленных зависимостей между определениями схем и сгенерированным кодом сущностей. Это означает, что ent/schema зависит от ent (потому что ему нужно использовать типы, предоставляемые фреймворком ent), в то время как сгенерированный код ent также зависит от ent/schema (потому что ему нужно получить доступ к информации схемы, определенной внутри нее).

Разрешение Циклических Зависимостей

Если вы столкнулись с ошибкой циклической зависимости, вы можете выполнить следующие шаги:

  1. Сначала закомментируйте все хуки, используемые в ent/schema.
  2. Затем переместите пользовательские типы, определенные в ent/schema, в новый пакет, например, вы можете создать пакет с именем ent/schema/schematype.
  3. Запустите команду go generate ./..., чтобы обновить пакет ent, чтобы он указывал на новый путь пакета, обновляя ссылки на типы в схеме. Например, измените schema.T на schematype.T.
  4. Раскомментируйте ранее закомментированные ссылки на хуки и снова выполните команду go generate ./.... На этом этапе кодовое поколение должно происходить без ошибок.

Следуя этим шагам, мы можем разрешить проблему циклической зависимости, вызванной импортом хуков, обеспечивая бесперебойную логику схемы и выполнение хуков.

5. Использование Вспомогательных Функций Хуков

Фреймворк ent предоставляет набор вспомогательных функций для хуков, которые могут помочь контролировать время выполнения хуков. Ниже приведены некоторые примеры часто используемых вспомогательных функций хуков:

// Выполнять HookA только для операций UpdateOne и DeleteOne
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

// Не выполнять HookB во время операции Create
hook.Unless(HookB(), ent.OpCreate)

// Выполнять HookC только тогда, когда мутация изменяет поле "status" и очищает поле "dirty"
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))

// Запретить изменение поля "password" в операциях Update (множественные)
hook.If(
    hook.FixedError(errors.New("нельзя изменять пароль при множественном обновлении")),
    hook.And(
        hook.HasOp(ent.OpUpdate),
        hook.Or(
            hook.HasFields("password"),
            hook.HasClearedFields("password"),
        ),
    ),
)

Эти вспомогательные функции позволяют точно контролировать условия активации хуков для разных операций.

6. Хуки Транзакций

Хуки транзакций позволяют выполнять определенные хуки при подтверждении транзакции (Tx.Commit) или откате (Tx.Rollback). Это очень полезно для обеспечения согласованности данных и атомарности операций.

Пример Хуков Транзакций

client.Tx(ctx, func(tx *ent.Tx) error {
    // Регистрация хука транзакции - хук `hookBeforeCommit` будет выполнен перед подтверждением.
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // Здесь можно поместить логику перед фактическим подтверждением.
            fmt.Println("Перед подтверждением")
            return next.Commit(ctx, tx)
        })
    })

    // Выполнение серии операций в пределах транзакции...

    return nil
})

В приведенном выше коде показано, как зарегистрировать хук транзакции для выполнения до подтверждения в транзакции. Этот хук будет вызван после выполнения всех операций базы данных и перед фактическим подтверждением транзакции.