1. Installing the ent tool

To install the ent code generation tool, you need to follow these steps:

First, make sure that your system has the Go language environment installed. Then, run the following command to obtain the ent tool:

go get -d entgo.io/ent/cmd/ent

This command will download the code for the ent tool, but it will not compile and install it immediately. If you want to install ent to the $GOPATH/bin directory so that you can use it anywhere, you also need to execute the following command:

go install entgo.io/ent/cmd/ent

Once the installation is complete, you can check if the ent tool is properly installed and view the available commands and instructions by running ent -h.

2. Initializing Schema

2.1 Initializing the Template Using ent init

Creating a new schema file is the first step to start using ent for code generation. You can initialize the schema template by executing the following command:

go run -mod=mod entgo.io/ent/cmd/ent new User Pet

This command will create two new schema files: user.go and pet.go, and place them in the ent/schema directory. If the ent directory does not exist, this command will also create it automatically.

Running the ent init command in the project's root directory is a good practice, as it helps maintain the project directory's structure and clarity.

2.2 Schema File Structure

In the ent/schema directory, each schema corresponds to a Go language source file. Schema files are where you define the database model, including the fields and edges (relationships).

For example, in the user.go file, you might define a user model, including fields such as username and age, and define the relationship between users and pets. Similarly, in the pet.go file, you would define the pet model and its related fields, such as the pet's name, type, and the relationship between pets and users.

These files will ultimately be used by the ent tool to generate corresponding Go code, including client code for database operations and CRUD (Create, Read, Update, Delete) operations.

// ent/schema/user.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/schema/field"
)

// User defines the schema for the User entity.
type User struct {
    ent.Schema
}

// The Fields method is used to define the entity's fields.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").Unique(),
        field.Int("age").Positive(),
    }
}

// The Edges method is used to define the entity's associations.
func (User) Edges() []ent.Edge {
    // Associations will be explained in more detail in the next section.
}

ent's schema files use Go language types and functions to declare the structure of the database model, including the fields and relationships between models, and use the API provided by the ent framework to define these structures. This approach makes ent very intuitive and easy to extend, while also taking advantage of the strong typing of Go language.

3.1 Running Code Generation

Running ent generate to generate code is a crucial step in the ent framework. With this command, ent will generate corresponding Go code files based on the defined schemas, facilitating subsequent development work. The command for executing code generation is straightforward:

go generate ./ent

The above command needs to be run in the project's root directory. When go generate is called, it will search for all Go files containing specific annotations and execute the commands specified in the annotations. In our example, this command specifies the code generator for ent.

Ensure that schema initialization and necessary field additions have been completed before execution. Only then will the generated code include all the necessary parts.

3.2 Understanding the Generated Code Assets

The generated code assets contain multiple components, each with different functions:

  • Client and Tx objects: Used to interact with the data graph. The Client provides methods to create transactions (Tx) or directly execute database operations.

  • CRUD builders: For each schema type, it generates builders for create, read, update, and delete, simplifying the operation logic of the corresponding entity.

  • Entity object (Go struct): It generates the corresponding Go structs for each type in the schema, mapping these structs to the tables in the database.

  • Constants and predicates package: Contains constants and predicates for interacting with the builders.

  • Migrate package: A package for database migration, suitable for SQL dialects.

  • Hook package: Provides the functionality to add change middleware, allowing custom logic to be executed before or after creating, updating, or deleting entities.

By examining the generated code, you can gain a deeper understanding of how the ent framework automates data access code for your schemas.

4. Code Generation Options

The ent generate command supports various options to customize the code generation process. You can query all supported generation options through the following command:

ent generate -h

Here are some commonly used command-line options:

  • --feature strings: Extends code generation, adding extra functionality.
  • --header string: Overrides the code generation header file.
  • --storage string: Specifies the storage driver supported in code generation, defaulting to "sql".
  • --target string: Specifies the target directory for code generation.
  • --template strings: Executes additional Go templates. Supports file, directory, and wildcard path, for example: --template file="path/to/file.tmpl".

These options allow developers to customize their code generation process according to different needs and preferences.

5. Storage Option Configuration

ent supports generating code assets for both SQL and Gremlin dialects, with the default being SQL dialect. If the project needs to connect to a Gremlin database, the corresponding database dialect needs to be configured. The following demonstrates how to specify storage options:

ent generate --storage gremlin ./ent/schema

The above command instructs ent to use the Gremlin dialect when generating code. This ensures that the generated assets are tailored to the requirements of the Gremlin database, ensuring compatibility with a specific graph database.

6. Advanced Usage: entc Package

6.1 Usage of entc as a Package in the Project

entc is the core package used for code generation in the ent framework. In addition to the command-line tool, entc can also be integrated into the project as a package, allowing developers to control and customize the code generation process within the code itself.

To use entc as a package in the project, you need to create a file named entc.go and add the following content to the file:

// +build ignore

package main

import (
    "log"
    "entgo.io/ent/entc"
    "entgo.io/ent/entc/gen"
)

func main() {
    if err := entc.Generate("./schema", &gen.Config{}); err != nil {
        log.Fatal("running ent codegen:", err)
    }
}

When using this approach, you can modify the gen.Config struct within the main function to apply different configuration options. By calling the entc.Generate function as needed, you can flexibly control the code generation process.

6.2 Detailed Configuration of entc

entc provides extensive configuration options, allowing developers to customize the generated code. For example, custom hooks can be configured to inspect or modify the generated code, and external dependencies can be injected using dependency injection.

The following example demonstrates how to provide custom hooks for the entc.Generate function:

func main() {
    err := entc.Generate("./schema", &gen.Config{
        Hooks: []gen.Hook{
            HookFunction,
        },
    })
    if err != nil {
        log.Fatalf("running ent codegen: %v", err)
    }
}

// HookFunction is a custom hook function
func HookFunction(next gen.Generator) gen.Generator {
    return gen.GenerateFunc(func(g *gen.Graph) error {
        // Can handle the graph mode represented by g here
        // For example, validate the existence of fields or modify the structure
        return next.Generate(g)
    })
}

Additionally, external dependencies can be added using entc.Dependency:

func main() {
    opts := []entc.Option{
        entc.Dependency(
            entc.DependencyType(&http.Client{}),
        ),
        entc.Dependency(
            entc.DependencyName("Writer"),
            entc.DependencyTypeInfo(&field.TypeInfo{
                Ident:   "io.Writer",
                PkgPath: "io",
            }),
        ),
    }
    if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
        log.Fatalf("running ent codegen: %v", err)
    }
}

In this example, we inject http.Client and io.Writer as dependencies into the generated client objects.

7. Output of Schema Description

In the ent framework, the ent describe command can be used to output the description of the schema in a graphical format. This can help developers quickly understand the existing entities and relationships.

Execute the following command to obtain the description of the schema:

go run -mod=mod entgo.io/ent/cmd/ent describe ./ent/schema

The above command will output a table similar to the following, displaying information such as fields, types, relationships, etc. for each entity:

User:
    +-------+---------+--------+-----------+ ...
    | Field |  Type   | Unique | Optional  | ...
    +-------+---------+--------+-----------+ ...
    | id    | int     | false  | false     | ...
    | name  | string  | true   | false     | ...
    +-------+---------+--------+-----------+ ...
    +-------+--------+---------+-----------+ ...
    | Edge  |  Type  | Inverse | Relation  | ...
    +-------+--------+---------+-----------+ ...
    | pets  | Pet    | false   | O2M       | ...
    +-------+--------+---------+-----------+ ...

8. Code Generation Hooks

8.1 Concept of Hooks

Hooks are middleware functions that can be inserted into the ent code generation process, allowing custom logic to be inserted before and after generating code. Hooks can be used to manipulate the abstract syntax tree (AST) of the generated code, perform validation, or add additional code snippets.

8.2 Example of Using Hooks

Here's an example of using a hook to ensure that all fields contain a certain struct tag (e.g., json):

func main() {
    err := entc.Generate("./schema", &gen.Config{
        Hooks: []gen.Hook{
            EnsureStructTag("json"),
        },
    })
    if err != nil {
        log.Fatalf("running ent codegen: %v", err)
    }
}

// EnsureStructTag ensures that all fields in the graph contain a specific struct tag
func EnsureStructTag(name string) gen.Hook {
    return func(next gen.Generator) gen.Generator {
        return gen.GenerateFunc(func(g *gen.Graph) error {
            for _, node := range g.Nodes {
                for _, field := range node.Fields {
                    tag := reflect.StructTag(field.StructTag)
                    if _, ok := tag.Lookup(name); !ok {
                        return fmt.Errorf("struct tag %q is missing for field %s.%s", name, node.Name, field.Name)
                    }
                }
            }
            return next.Generate(g)
        })
    }
}

In this example, before generating the code, the EnsureStructTag function checks each field for the json tag. If a field is missing this tag, the code generation will terminate and return an error. This is an effective way to maintain code cleanliness and consistency.