1. Introduction to ent

Ent is an entity framework developed by Facebook specifically for the Go language. It simplifies the process of building and maintaining large-scale data model applications. The ent framework mainly follows the following principles:

  • Easily model the database schema as a graph structure.
  • Define the schema in the form of Go language code.
  • Implement static types based on code generation.
  • Writing database queries and graph traversal is very simple.
  • Easily extend and customize using Go templates.

2. Environment Setup

To start using the ent framework, make sure that the Go language is installed in your development environment.

If your project directory is outside of GOPATH, or if you are not familiar with GOPATH, you can use the following command to create a new Go module project:

go mod init entdemo

This will initialize a new Go module and create a new go.mod file for your entdemo project.

3. Defining the First Schema

3.1. Creating Schema Using ent CLI

First, you need to run the following command in the root directory of your project to create a schema named User using the ent CLI tool:

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

The above command will generate the User schema in the entdemo/ent/schema/ directory:

File entdemo/ent/schema/user.go:

package schema

import "entgo.io/ent"

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

// Fields of the User.
func (User) Fields() []ent.Field {
    return nil
}

// Edges of the User.
func (User) Edges() []ent.Edge {
    return nil
}

3.2. Adding Fields

Next, we need to add field definitions to the User Schema. Below is an example of adding two fields to the User entity.

File modified entdemo/ent/schema/user.go:

package schema

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

// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("name").
            Default("unknown"),
    }
}

This piece of code defines two fields for the User model: age and name, where age is a positive integer and name is a string with a default value of “unknown”.

3.3. Generating Database Entities

After defining the schema, you need to run the go generate command to generate the underlying database access logic.

Run the following command in the root directory of your project:

go generate ./ent

This command will generate the corresponding Go code based on the previously defined schema, resulting in the following file structure:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (several files omitted for brevity)
├── schema
│   └── user.go
├── tx.go
├── user
│   ├── user.go
│   └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go

4.1. Initializing Database Connection

To establish a connection to the MySQL database, we can use the Open function provided by the ent framework. First, import the MySQL driver and then provide the correct connection string to initialize the database connection.

package main

import (
    "context"
    "log"

    "entdemo/ent"

    _ "github.com/go-sql-driver/mysql" // Import the MySQL driver
)

func main() {
    // Use ent.Open to establish a connection with the MySQL database.
    // Remember to replace the placeholders "your_username", "your_password", and "your_database" below.
    client, err := ent.Open("mysql", "your_username:your_password@tcp(localhost:3306)/your_database?parseTime=True")
    if err != nil {
        log.Fatalf("failed opening connection to mysql: %v", err)
    }
    defer client.Close()

    // Run the automatic migration tool
    ctx := context.Background()
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("failed creating schema resources: %v", err)
    }

    // Additional business logic can be written here
}

4.2. Creating Entities

Creating a User entity involves building a new entity object and persisting it to the database using the Save or SaveX method. The following code demonstrates how to create a new User entity and initialize two fields age and name.

// The CreateUser function is used to create a new User entity
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Use client.User.Create() to build the request for creating a User,
    // then chain the SetAge and SetName methods to set the values of entity fields.
    u, err := client.User.
        Create().
        SetAge(30).    // Set user age
        SetName("a8m"). // Set user name
        Save(ctx)     // Call Save to save the entity to the database
    if err != nil {
        return nil, fmt.Errorf("failed creating user: %w", err)
    }
    log.Println("user was created: ", u)
    return u, nil
}

In the main function, you can call the CreateUser function to create a new user entity.

func main() {
    // ...Omitted database connection establishment code

    // Create a user entity
    u, err := CreateUser(ctx, client)
    if err != nil {
        log.Fatalf("failed creating user: %v", err)
    }
    log.Printf("created user: %#v\n", u)
}

4.3. Querying Entities

To query entities, we can use the query builder generated by ent. The following code demonstrates how to query a user named “a8m”:

// The QueryUser function is used to query the user entity with a specified name
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Use client.User.Query() to build the query for User,
    // then chain the Where method to add query conditions, such as querying by username
    u, err := client.User.
        Query().
        Where(user.NameEQ("a8m")).      // Add query condition, in this case, the name is "a8m"
        Only(ctx)                      // The Only method indicates that only one result is expected
    if err != nil {
        return nil, fmt.Errorf("failed querying user: %w", err)
    }
    log.Println("user returned: ", u)
    return u, nil
}

In the main function, you can call the QueryUser function to query the user entity.

func main() {
    // ...Omitted database connection establishment and user creation code

    // Query the user entity
    u, err := QueryUser(ctx, client)
    if err != nil {
        log.Fatalf("failed querying user: %v", err)
    }
    log.Printf("queried user: %#v\n", u)
}

5.1. Understanding Edges and Inverse Edges

In the ent framework, the data model is visualized as a graph structure, where entities represent nodes in the graph, and the relationships between entities are represented by edges. An edge is a connection from one entity to another, for example, a User can own multiple Cars.

Inverse Edges are reverse references to edges, logically representing the reverse relationship between entities, but not creating a new relationship in the database. For example, through the inverse edge of a Car, we can find the User who owns this car.

The key significance of edges and inverse edges lies in making navigation between associated entities very intuitive and straightforward.

Tip: In ent, edges correspond to traditional database foreign keys and are used to define relationships between tables.

5.2. Defining Edges in the Schema

First, we will use the ent CLI to create the initial schema for Car and Group:

go run -mod=mod entgo.io/ent/cmd/ent new Car Group

Next, in the User schema, we define the edge with Car to represent the relationship between users and cars. We can add an edge cars pointing to the Car type in the user entity, indicating that a user can have multiple cars:

// entdemo/ent/schema/user.go

// Edges of the User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("cars", Car.Type),
    }
}

After defining the edges, we need to run go generate ./ent again to generate the corresponding code.

5.3. Operating on Edge Data

Creating cars associated with a user is a simple process. Given a user entity, we can create a new car entity and associate it with the user:

import (
    "context"
    "log"
    "entdemo/ent"
    // Make sure to import the schema definition for Car
    _ "entdemo/ent/schema"
)

func CreateCarsForUser(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("failed getting user: %v", err)
        return err
    }

    // Create a new car and associate it with the user
    _, err = client.Car.
        Create().
        SetModel("Tesla").
        SetRegisteredAt(time.Now()).
        SetOwner(user).
        Save(ctx)
    if err != nil {
        log.Fatalf("failed creating car for user: %v", err)
        return err
    }

    log.Println("car was created and associated with the user")
    return nil
}

Similarly, querying a user’s cars is straightforward. If we want to retrieve a list of all the cars owned by a user, we can do the following:

func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("failed getting user: %v", err)
        return err
    }

    // Query all cars owned by the user
    cars, err := user.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("failed querying cars: %v", err)
        return err
    }

    for _, car := range cars {
        log.Printf("car: %v, model: %v", car.ID, car.Model)
    }
    return nil
}

Through the above steps, we have not only learned how to define edges in the schema but also demonstrated how to create and query data related to edges.

6. Graph Traversal and Querying

6.1. Understanding Graph Structures

In ent, graph structures are represented by entities and the edges between them. Each entity is equivalent to a node in the graph, and the relationships between entities are represented by edges, which can be one-to-one, one-to-many, many-to-many, etc. This graph structure makes complex queries and operations on a relational database simple and intuitive.

6.2. Traversing Graph Structures

Writing Graph Traversal code mainly involves querying and associating data through the edges between entities. Below is a simple example demonstrating how to traverse the graph structure in ent:

import (
    "context"
    "log"

    "entdemo/ent"
)

// GraphTraversal is an example of traversing the graph structure
func GraphTraversal(ctx context.Context, client *ent.Client) error {
    // Query the user named "Ariel"
    a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
    if err != nil {
        log.Fatalf("Failed querying user: %v", err)
        return err
    }

    // Traverse all the cars belonging to Ariel
    cars, err := a8m.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("Failed querying cars: %v", err)
        return err
    }
    for _, car := range cars {
        log.Printf("Ariel has a car with model: %s", car.Model)
    }

    // Traverse all the groups Ariel is a member of
    groups, err := a8m.QueryGroups().All(ctx)
    if err != nil {
        log.Fatalf("Failed querying groups: %v", err)
        return err
    }
    for _, g := range groups {
        log.Printf("Ariel is a member of group: %s", g.Name)
    }

    return nil
}

The above code is a basic example of graph traversal, which first queries a user and then traverses the user’s cars and groups.

7. Visualizing Database Schema

7.1. Installing the Atlas Tool

To visualize the database schema generated by ent, we can use the Atlas tool. The installation steps for Atlas are very simple. For example, on macOS, you can install it using brew:

brew install ariga/tap/atlas

Note: Atlas is a universal database migration tool that can handle table structure version management for various databases. Detailed introduction to Atlas will be provided in later chapters.

7.2. Generating ERD and SQL Schema

Using Atlas to view and export schemas is very straightforward. After installing Atlas, you can use the following command to view the Entity-Relationship Diagram (ERD):

atlas schema inspect -d [database_dsn] --format dot

Or directly generate the SQL Schema:

atlas schema inspect -d [database_dsn] --format sql

Where [database_dsn] points to the data source name (DSN) of your database. For example, for an SQLite database, it might be:

atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot

The output generated by these commands can be further transformed into views or documents using the respective tools.

8. Schema Migration

8.1. Automatic Migration and Versioned Migration

ent supports two schema migration strategies: automatic migration and versioned migration. Automatic migration is the process of inspecting and applying schema changes at runtime, suitable for development and testing. Versioned migration involves generating migration scripts and requires careful review and testing before production deployment.

Tip: For automatic migration, refer to the content in section 4.1.

8.2. Performing Versioned Migration

The process of versioned migration involves generating migration files through Atlas. Below are the relevant commands:

To generate migration files:

atlas migrate diff -d ent/schema/path --dir migrations/dir

Subsequently, these migration files can be applied to the database:

atlas migrate apply -d migrations/dir --url database_dsn

Following this process, you can maintain a history of database migrations in the version control system and ensure thorough review before each migration.

Tip: Refer to the sample code at https://github.com/ent/ent/tree/master/examples/start