1. Basic Concepts of Entity and Association

In the ent framework, an entity refers to the basic data unit managed in the database, which usually corresponds to a table in the database. The fields in the entity correspond to the columns in the table, while the associations (edges) between entities are used to describe the relationships and dependencies between entities. Entity associations form the foundation for constructing complex data models, allowing for the representation of hierarchical relationships such as parent-child relationships and ownership relationships.

The ent framework provides a rich set of APIs, allowing developers to define and manage these associations in the entity schema. Through these associations, we can easily express and operate on the complex business logic between data.

2. Types of Entity Associations in ent

2.1 One-to-One (O2O) Association

A one-to-one association refers to a one-to-one correspondence between two entities. For example, in the case of users and bank accounts, each user can only have one bank account, and each bank account also belongs to only one user. The ent framework uses the edge.To and edge.From methods to define such associations.

First, we can define a one-to-one association pointing to Card within the User schema:

// Edges of the User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("card", Card.Type). // Points to the Card entity, defining the association name as "card"
            Unique(),               // The Unique method ensures this is a one-to-one association
    }
}

Next, we define the reverse association back to User within the Card schema:

// Edges of the Card.
func (Card) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Points back to User from Card, defining the association name as "owner"
            Ref("card").              // The Ref method specifies the corresponding reverse association name
            Unique(),                 // Marked as unique to ensure one card corresponds to one owner
    }
}

2.2 One-to-Many (O2M) Association

A one-to-many association indicates that one entity can be associated with multiple other entities, but these entities can only point back to a single entity. For example, a user may have multiple pets, but each pet only has one owner.

In ent, we still use edge.To and edge.From to define this type of association. The example below defines a one-to-many association between users and pets:

// Edges of the User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("pets", Pet.Type), // One-to-many association from User entity to Pet entity
    }
}

In the Pet entity, we define a many-to-one association back to User:

// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Many-to-one association from Pet to User
            Ref("pets").              // Specifies the reverse association name from pet to owner
            Unique(),                 // Ensures one owner can have multiple pets
    }
}

2.3 Many-to-Many (M2M) Association

A many-to-many association allows two types of entities to have multiple instances of each other. For example, a student can enroll in multiple courses, and a course can also have multiple students enrolled. ent provides an API for establishing many-to-many associations:

In the Student entity, we use edge.To to establish a many-to-many association with Course:

// Edges of the Student.
func (Student) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("courses", Course.Type), // Define a many-to-many association from Student to Course
    }
}

Similarly, in the Course entity, we establish a reverse association to Student for the many-to-many relationship:

// Edges of the Course.
func (Course) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("students", Student.Type). // Define a many-to-many association from Course to Student
            Ref("courses"),                  // Specify the reverse association name from Course to Student
    }
}

These types of associations are the cornerstone of building complex application data models, and understanding how to define and use them in ent is crucial for extending data models and business logic.

3. Basic Operations for Entity Associations

This section will demonstrate how to perform basic operations using ent with the defined relationships, including creating, querying, and traversing associated entities.

3.1 Creating Associated Entities

When creating entities, you can simultaneously set the relationships between entities. For one-to-many (O2M) and many-to-many (M2M) relationships, you can use the Add{Edge} method to add associated entities.

For example, if we have a user entity and a pet entity with a certain association, where a user can have multiple pets, the following is an example of creating a new user and adding pets for them:

// Create a user and add pets
func CreateUserWithPets(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Create a pet instance
    fido := client.Pet.
        Create().  
        SetName("Fido").
        SaveX(ctx)
    // Create a user instance and associate it with the pet
    user := client.User.
        Create().
        SetName("Alice").
        AddPets(fido). // Use the AddPets method to associate the pet
        SaveX(ctx)

    return user, nil
}

In this example, we first create a pet instance named Fido, then create a user named Alice and associate the pet instance with the user using the AddPets method.

3.2 Querying Associated Entities

Querying associated entities is a common operation in ent. For example, you can use the Query{Edge} method to retrieve other entities associated with a specific entity.

Continuing with our example of users and pets, here's how to query all the pets owned by a user:

// Query all pets of a user
func QueryUserPets(ctx context.Context, client *ent.Client, userID int) ([]*ent.Pet, error) {
    pets, err := client.User.
        Get(ctx, userID). // Get the user instance based on the user ID
        QueryPets().      // Query the pet entities associated with the user
        All(ctx)          // Return all the queried pet entities
    if err != nil {
        return nil, err
    }

    return pets, nil
}

In the above code snippet, we first get the user instance based on the user ID, then call the QueryPets method to retrieve all pet entities associated with that user.

Note: ent's code generation tool automatically generates the API for association queries based on the defined entity relationships. It's recommended to review the generated code.

4. Eager Loading

4.1 Principles of Preloading

Preloading is a technique used in querying databases to fetch and load associated entities in advance. This approach is commonly employed to obtain data related to multiple entities in one go, in order to avoid multiple database query operations in subsequent processing, thereby significantly enhancing the application's performance.

In the ent framework, preloading is primarily used to handle relationships between entities, such as one-to-many and many-to-many. When retrieving an entity from the database, its associated entities are not automatically loaded. Instead, they are explicitly loaded as needed through preloading. This is crucial for alleviating the N+1 query problem (i.e., performing separate queries for each parent entity).

In the ent framework, preloading is achieved by using the With method in the query builder. This method generates corresponding With... functions for each edge, such as WithGroups and WithPets. These methods are automatically generated by the ent framework, and programmers can use them to request preloading of specific associations.

The working principle of preloading entities is that when querying the primary entity, ent executes additional queries to retrieve all associated entities. Subsequently, these entities are populated into the Edges field of the returned object. This means that ent may execute multiple database queries, at least once for each associated edge that needs to be preloaded. While this method may be less efficient than a single complex JOIN query in certain scenarios, it offers greater flexibility and is expected to receive performance optimizations in future versions of ent.

4.2 Implementation of Preloading

We will now demonstrate how to perform preloading operations in the ent framework through some example code, using the models of users and pets outlined in the overview.

Preloading a Single Association

Suppose we want to retrieve all users from the database and preload the pet data. We can achieve this by writing the following code:

users, err := client.User.
    Query().
    WithPets().
    All(ctx)
if err != nil {
    // Handle the error
    return err
}
for _, u := range users {
    for _, p := range u.Edges.Pets {
        fmt.Printf("User (%v) owns pet (%v)\n", u.ID, p.ID)
    }
}

In this example, we use the WithPets method to request ent to preload the pet entities associated with the users. The preloaded pet data is populated into the Edges.Pets field, from which we can access this associated data.

Preloading Multiple Associations

ent allows us to preload multiple associations at once, and even specify preloading nested associations, filtering, sorting, or limiting the number of preloaded results. Below is an example of preloading the pets of administrators and the teams they belong to, while also preloading the users associated with the teams:

admins, err := client.User.
    Query().
    Where(user.Admin(true)).
    WithPets().
    WithGroups(func(q *ent.GroupQuery) {
        q.Limit(5)          // Limit to the first 5 teams
        q.Order(ent.Asc(group.FieldName)) // Sort in ascending order by team name
        q.WithUsers()       // Preload the users in the team
    }).
    All(ctx)
if err != nil {
    // Handle errors
    return err
}
for _, admin := range admins {
    for _, p := range admin.Edges.Pets {
        fmt.Printf("Admin (%v) owns pet (%v)\n", admin.ID, p.ID)
    }
    for _, g := range admin.Edges.Groups {
        fmt.Printf("Admin (%v) belongs to team (%v)\n", admin.ID, g.ID)
        for _, u := range g.Edges.Users {
            fmt.Printf("Team (%v) has member (%v)\n", g.ID, u.ID)
        }
    }
}

Through this example, you can see how powerful and flexible ent is. With just a few simple method calls, it can preload rich associated data and organize them in a structured way. This provides great convenience for developing data-driven applications.