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:

  1. Defining the hook function. This function takes an ent.Mutator and returns an ent.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
    })
}
  1. 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 the Hooks 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)
    })
})
  1. 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:

  1. First, comment out all hooks used in ent/schema.
  2. Next, move the custom types defined in ent/schema to a new package, for example, you can create a package named ent/schema/schematype.
  3. Run the go generate ./... command to update the ent package, so that it points to the new package path, updating the type references in the schema. For example, change schema.T to schematype.T.
  4. 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.