1. Khái niệm cơ bản về Thực thể và Liên kết

Trong framework ent, một thực thể đề cập đến đơn vị dữ liệu cơ bản được quản lý trong cơ sở dữ liệu, thường tương ứng với một bảng trong cơ sở dữ liệu. Các trường trong thực thể tương ứng với các cột trong bảng, trong khi các liên kết (cạnh) giữa các thực thể được sử dụng để mô tả mối quan hệ và sự phụ thuộc giữa các thực thể. Các liên kết thực thể tạo nên cơ sở để xây dựng các mô hình dữ liệu phức tạp, cho phép biểu diễn các mối quan hệ phân cấp như mối quan hệ cha - con và mối quan hệ sở hữu.

Framework ent cung cấp một bộ API phong phú, cho phép các nhà phát triển xác định và quản lý các liên kết này trong schema của thực thể. Thông qua các liên kết này, chúng ta có thể dễ dàng diễn đạt và thực hiện các logic kinh doanh phức tạp giữa dữ liệu.

2. Các loại Liên kết Thực thể trong ent

2.1 Liên kết Một-cho-Một (O2O)

Liên kết một-cho-một đề cập đến sự tương ứng một-một giữa hai thực thể. Ví dụ, trong trường hợp người dùng và tài khoản ngân hàng, mỗi người dùng chỉ có thể có một tài khoản ngân hàng và mỗi tài khoản ngân hàng cũng chỉ thuộc về một người dùng. Framework ent sử dụng các phương thức edge.Toedge.From để xác định các liên kết như vậy.

Trước tiên, chúng ta có thể xác định một liên kết một-cho-một trỏ tới Card trong schema của User:

// Cạnh của User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("card", Card.Type). // Trỏ tới thực thể Card, xác định tên liên kết là "card"
            Unique(),               // Phương thức Unique đảm bảo rằng đây là một liên kết một-cho-một
    }
}

Tiếp theo, chúng ta xác định liên kết đảo ngược trở lại User trong schema của Card:

// Cạnh của Card.
func (Card) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Trỏ lại User từ Card, xác định tên liên kết là "owner"
            Ref("card").                // Phương thức Ref chỉ định tên liên kết đảo ngược tương ứng
            Unique(),                   // Đánh dấu là duy nhất để đảm bảo một thẻ tương ứng với một chủ sở hữu
    }
}

2.2 Liên kết Một-cho-Nhiều (O2M)

Một liên kết một-cho-nhiều cho biết rằng một thực thể có thể liên kết với nhiều thực thể khác, nhưng những thực thể này chỉ có thể trỏ lại một thực thể duy nhất. Ví dụ, người dùng có thể có nhiều thú nuôi, nhưng mỗi thú nuôi chỉ có một chủ sở hữu.

Trong ent, chúng ta vẫn sử dụng edge.Toedge.From để xác định loại liên kết này. Ví dụ dưới đây xác định một liên kết một-cho-nhiều giữa người dùng và thú nuôi:

// Cạnh của User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("pets", Pet.Type), // Liên kết một-nhiều từ thực thể User đến thực thể Pet
    }
}

Trong thực thể Pet, chúng ta xác định một liên kết nhiều-cho-một trở lại User:

// Cạnh của Pet.
func (Pet) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // Liên kết nhiều-cho-một từ Pet đến User
            Ref("pets").                // Chỉ định tên liên kết đảo ngược từ pet đến chủ sở hữu
            Unique(),                   // Đảm bảo một chủ sở hữu có thể có nhiều thú nuôi
    }
}

2.3 Liên kết Nhiều-cho-Nhiều (M2M)

Một liên kết nhiều-cho-nhiều cho phép hai loại thực thể có nhiều phiên bản của nhau. Ví dụ, một học sinh có thể đăng ký nhiều khóa học và một khóa học cũng có thể có nhiều học sinh đăng ký. ent cung cấp một API để thiết lập các liên kết nhiều-cho-nhiều:

Trong thực thể Student, chúng ta sử dụng edge.To để thiết lập một liên kết nhiều-cho-nhiều với Course:

// Cạnh của Student.
func (Student) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("courses", Course.Type), // Xác định một liên kết nhiều-cho-nhiều từ Student đến Course
    }
}

Tương tự, trong thực thể Course, chúng ta thiết lập một liên kết đảo ngược đến Student cho mối quan hệ nhiều-cho-nhiều:

// Cạnh của Course.
func (Course) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("students", Student.Type). // Xác định một liên kết nhiều-cho-nhiều từ Course đến Student
            Ref("courses"),                  // Chỉ định tên liên kết đảo ngược từ Course đến Student
    }
}

Những loại liên kết này là nền tảng của việc xây dựng các mô hình dữ liệu ứng dụng phức tạp, và hiểu cách xác định và sử dụng chúng trong ent là quan trọng để mở rộng các mô hình dữ liệu và logic kinh doanh.

3. Các Hoạt Động Cơ Bản cho Các Mối Quan Hệ Giữa Thực Thể

Phần này sẽ hướng dẫn cách thực hiện các hoạt động cơ bản sử dụng ent với các mối quan hệ đã được xác định, bao gồm việc tạo, truy vấn và duyệt qua các thực thể liên quan.

3.1 Tạo Các Thực Thể Liên Quan

Khi tạo các thực thể, bạn có thể đồng thời thiết lập mối quan hệ giữa chúng. Đối với các mối quan hệ một-nhiều (O2M) và nhiều-nhiều (M2M), bạn có thể sử dụng phương thức Add{Edge} để thêm các thực thể liên quan.

Ví dụ, nếu chúng ta có một thực thể người dùng và một thực thể thú cưng với mối quan hệ cụ thể, trong đó người dùng có thể sở hữu nhiều thú cưng, sau đây là một ví dụ về việc tạo một người dùng mới và thêm thú cưng cho họ:

// Tạo một người dùng và thêm thú cưng
func CreateUserWithPets(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Tạo một thực thể thú cưng
    fido := client.Pet.
        Create().  
        SetName("Fido").
        SaveX(ctx)
    // Tạo một thực thể người dùng và liên kết nó với thú cưng
    user := client.User.
        Create().
        SetName("Alice").
        AddPets(fido). // Sử dụng phương thức AddPets để liên kết thú cưng
        SaveX(ctx)

    return user, nil
}

Trong ví dụ này, trước tiên chúng ta tạo một thực thể thú cưng có tên là Fido, sau đó tạo một người dùng có tên là Alice và liên kết thực thể thú cưng với người dùng bằng cách sử dụng phương thức AddPets.

3.2 Truy Vấn Các Thực Thể Liên Quan

Việc truy vấn các thực thể liên quan là một hoạt động phổ biến trong ent. Ví dụ, bạn có thể sử dụng phương thức Query{Edge} để truy xuất các thực thể khác liên kết với một thực thể cụ thể.

Tiếp tục với ví dụ về người dùng và thú cưng, dưới đây là cách truy vấn tất cả thú cưng mà một người dùng sở hữu:

// Truy vấn tất cả thú cưng của một người dùng
func QueryUserPets(ctx context.Context, client *ent.Client, userID int) ([]*ent.Pet, error) {
    pets, err := client.User.
        Get(ctx, userID). // Lấy thực thể người dùng dựa trên ID người dùng
        QueryPets().      // Truy vấn các thực thể thú cưng liên kết với người dùng
        All(ctx)          // Trả về tất cả các thực thể thú cưng đã truy vấn
    if err != nil {
        return nil, err
    }

    return pets, nil
}

Trong đoạn mã trên, trước tiên chúng ta lấy thực thể người dùng dựa trên ID người dùng, sau đó gọi phương thức QueryPets để truy xuất tất cả thực thể thú cưng liên kết với người dùng đó.

Lưu ý: Công cụ sinh mã của ent tự động tạo ra API để truy vấn liên kết dựa trên các mối quan hệ thực thể đã xác định. Đề nghị kiểm tra mã đã được tạo ra.

4. Tải Trước (Eager Loading)

4.1 Nguyên Tắc của Tải Trước

Tải trước là một kỹ thuật được sử dụng trong việc truy vấn cơ sở dữ liệu để lấy và tải các thực thể liên quan trước. Phương pháp này thường được sử dụng để lấy dữ liệu liên quan đến nhiều thực thể trong một lần, nhằm tránh nhiều hoạt động truy vấn cơ sở dữ liệu riêng lẻ trong các xử lý tiếp theo, từ đó cải thiện đáng kể hiệu suất của ứng dụng.

Trong khung công cụ ent, việc tải trước chủ yếu được sử dụng để xử lý mối quan hệ giữa các thực thể, chẳng hạn như mối quan hệ một-nhiều và nhiều-nhiều. Khi lấy một thực thể từ cơ sở dữ liệu, các thực thể liên quan của nó không được tải tự động. Thay vào đó, chúng được tải tường minh khi cần thông qua việc tải trước. Điều này rất quan trọng để giảm vấn đề truy vấn N+1 (tức là thực hiện các truy vấn riêng lẻ cho mỗi thực thể cha).

Trong khung công cụ ent, việc tải trước được thực hiện bằng cách sử dụng phương thức With trong trình xây dựng truy vấn. Phương thức này tạo ra các hàm With... tương ứng cho mỗi cạnh, chẳng hạn như WithGroupsWithPets. Các phương thức này được tự động tạo ra bởi khung công cụ ent, và các lập trình viên có thể sử dụng chúng để yêu cầu tải trước các liên kết cụ thể.

Nguyên lý hoạt động của việc tải trước các thực thể là khi truy vấn thực thể chính, ent thực hiện các truy vấn bổ sung để lấy tất cả các thực thể liên quan. Sau đó, các thực thể này được điền vào trường Edges của đối tượng được trả về. Điều này có nghĩa là ent có thể thực hiện nhiều truy vấn cơ sở dữ liệu, ít nhất một lần cho mỗi cạnh liên kết cần phải được tải trước. Mặc dù phương pháp này có thể không hiệu quả bằng một truy vấn JOIN phức tạp duy nhất trong một số tình huống, nhưng nó cung cấp tính linh hoạt cao hơn và được kỳ vọng sẽ nhận được các tối ưu hiệu suất trong các phiên bản tương lai của ent.

4.2 Thực Hiện Của Tải Trước

Bây giờ chúng ta sẽ thể hiện cách thực hiện các hoạt động tải trước trong khung công cụ ent thông qua một số mã ví dụ, sử dụng các mô hình về người dùng và thú cưng đã được đề cập trong phần tổng quan.

Nạp trước một Liên kết Đơn

Giả sử chúng ta muốn lấy tất cả người dùng từ cơ sở dữ liệu và nạp trước dữ liệu vật nuôi. Chúng ta có thể đạt được điều này bằng cách viết mã sau:

users, err := client.User.
    Query().
    WithPets().
    All(ctx)
if err != nil {
    // Xử lý lỗi
    return err
}
for _, u := range users {
    for _, p := range u.Edges.Pets {
        fmt.Printf("Người dùng (%v) sở hữu vật nuôi (%v)\n", u.ID, p.ID)
    }
}

Trong ví dụ này, chúng ta sử dụng phương thức WithPets để yêu cầu ent nạp trước các thực thể vật nuôi liên kết với người dùng. Dữ liệu vật nuôi đã được nạp trước được điền vào trường Edges.Pets, từ đó chúng ta có thể truy cập vào dữ liệu liên kết này.

Nạp trước Nhiều Liên kết

ent cho phép chúng ta nạp trước nhiều liên kết cùng một lúc, và thậm chí có thể chỉ định nạp trước các liên kết lồng nhau, lọc, sắp xếp, hoặc giới hạn số kết quả đã được nạp trước. Dưới đây là một ví dụ về việc nạp trước các vật nuôi của quản trị viên và các nhóm mà họ thuộc vào, đồng thời cũng nạp trước người dùng liên kết với các nhóm:

admins, err := client.User.
    Query().
    Where(user.Admin(true)).
    WithPets().
    WithGroups(func(q *ent.GroupQuery) {
        q.Limit(5)          // Giới hạn chỉ 5 nhóm đầu tiên
        q.Order(ent.Asc(group.FieldName)) // Sắp xếp theo thứ tự tên nhóm tăng dần
        q.WithUsers()       // Nạp trước người dùng trong nhóm
    }).
    All(ctx)
if err != nil {
    // Xử lý lỗi
    return err
}
for _, admin := range admins {
    for _, p := range admin.Edges.Pets {
        fmt.Printf("Quản trị viên (%v) sở hữu vật nuôi (%v)\n", admin.ID, p.ID)
    }
    for _, g := range admin.Edges.Groups {
        fmt.Printf("Quản trị viên (%v) thuộc nhóm (%v)\n", admin.ID, g.ID)
        for _, u := range g.Edges.Users {
            fmt.Printf("Nhóm (%v) có thành viên (%v)\n", g.ID, u.ID)
        }
    }
}

Qua ví dụ này, bạn có thể thấy ent mạnh mẽ và linh hoạt như thế nào. Với chỉ vài cuộc gọi phương thức đơn giản, nó có thể nạp trước dữ liệu liên kết phong phú và tổ chức chúng một cách có cấu trúc. Điều này mang lại sự thuận tiện lớn cho việc phát triển ứng dụng dữ liệu.