1. Hooks Mechanism
The Hooks
mechanism is a method for adding custom logic before or after specific changes occur in database operations. When modifying the database schema, such as adding new nodes, deleting edges between nodes, or deleting multiple nodes, we can use Hooks
to perform data validation, log recording, permission checks, or any custom operations. This is crucial for ensuring data consistency and compliance with business rules, while also allowing developers to add additional functionality without altering the original business logic.
2. Hook Registration Method
2.1 Global Hooks and Local Hooks
Global hooks (Runtime hooks
) are effective for all types of operations in the graph. They are suitable for adding logic to the entire application, such as logging and monitoring. Local hooks (Schema hooks
) are defined within specific type schemas and only apply to mutation operations that match the type of the schema. Using local hooks allows all logic related to specific node types to be centralized in one place, namely in the schema definition.
2.2 Steps for Registering Hooks
Registering a hook in the code usually involves the following steps:
- Defining the hook function. This function takes an
ent.Mutator
and returns anent.Mutator
. For example, creating a simple logging hook:
logHook := func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// Print logs before the mutation operation
log.Printf("Before mutating: Type=%s, Operation=%s\n", m.Type(), m.Op())
// Perform the mutation operation
v, err := next.Mutate(ctx, m)
// Print logs after the mutation operation
log.Printf("After mutating: Type=%s, Operation=%s\n", m.Type(), m.Op())
return v, err
})
}
- Register the hook with the client. For global hooks, you can register them using the
Use
method of the client. For local hooks, you can register them in the schema using theHooks
method of the type.
// Register a global hook
client.Use(logHook)
// Register a local hook, only applied to the User type
client.User.Use(func(next ent.Mutator) ent.Mutator {
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
// Add specific logic
// ...
return next.Mutate(ctx, m)
})
})
- You can chain multiple hooks, and they will be executed in the order of registration.
3. Execution Order of Hooks
The execution order of hooks is determined by the order in which they are registered with the client. For example, client.Use(f, g, h)
will execute on the mutation operation in the order of f(g(h(...)))
. In this example, f
is the first hook to be executed, followed by g
, and finally h
.
It is important to note that runtime hooks (Runtime hooks
) take precedence over schema hooks (Schema hooks
). This means that if g
and h
are hooks defined in the schema while f
is registered using client.Use(...)
, the execution order will be f(g(h(...)))
. This ensures that global logic, such as logging, is executed before all other hooks.
4. Dealing with Issues Caused by Hooks
When customizing database operations using Hooks, we may encounter the issue of import cycles. This usually occurs when attempting to use schema hooks, as the ent/schema
package may introduce the ent
core package. If the ent
core package also attempts to import ent/schema
, a circular dependency is formed.
Causes of Circular Dependencies
Circular dependencies usually arise from bidirectional dependencies between schema definitions and generated entity code. This means that ent/schema
depends on ent
(because it needs to use types provided by the ent
framework), while the code generated by ent
also depends on ent/schema
(because it needs to access the schema information defined within it).
Resolving Circular Dependencies
If you encounter a circular dependency error, you can follow these steps:
- First, comment out all hooks used in
ent/schema
. - Next, move the custom types defined in
ent/schema
to a new package, for example, you can create a package namedent/schema/schematype
. - Run the
go generate ./...
command to update theent
package, so that it points to the new package path, updating the type references in the schema. For example, changeschema.T
toschematype.T
. - Uncomment the previously commented hooks references and run the
go generate ./...
command again. At this point, the code generation should proceed without errors.
By following these steps, we can resolve the circular dependency issue caused by Hooks imports, ensuring that the logic of the schema and the implementation of Hooks can proceed smoothly.
5. Usage of Hook Helper Functions
The ent
framework provides a set of hook helper functions, which can help us control the timing of Hooks execution. Below are some examples of commonly used Hook helper functions:
// Only execute HookA for UpdateOne and DeleteOne operations
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)
// Do not execute HookB during Create operation
hook.Unless(HookB(), ent.OpCreate)
// Execute HookC only when the Mutation is altering the "status" field and clearing the "dirty" field
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))
// Prohibit altering the "password" field in Update (multiple) operations
hook.If(
hook.FixedError(errors.New("password cannot be edited on update many")),
hook.And(
hook.HasOp(ent.OpUpdate),
hook.Or(
hook.HasFields("password"),
hook.HasClearedFields("password"),
),
),
)
These helper functions allow us to precisely control the activation conditions of Hooks for different operations.
6. Transaction Hooks
Transaction Hooks allow specific Hooks to be executed when a transaction is committed (Tx.Commit
) or rolled back (Tx.Rollback
). This is very useful for ensuring data consistency and the atomicity of operations.
Example of Transaction Hooks
client.Tx(ctx, func(tx *ent.Tx) error {
// Registering a transaction hook - the hookBeforeCommit will be executed before the commit.
tx.OnCommit(func(next ent.Committer) ent.Committer {
return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
// The logic before the actual commit can be placed here.
fmt.Println("Before commit")
return next.Commit(ctx, tx)
})
})
// Perform a series of operations within the transaction...
return nil
})
The above code shows how to register a transaction hook to run before a commit in a transaction. This hook will be called after all database operations are executed and before the transaction is actually committed.