1. Giới thiệu về ent

Ent là một framework entity được phát triển bởi Facebook dành riêng cho ngôn ngữ Go. Nó giúp đơn giản hóa quá trình xây dựng và duy trì các ứng dụng mô hình dữ liệu quy mô lớn. Ent framework chủ yếu tuân theo các nguyên tắc sau:

  • Dễ dàng mô hình hóa schema cơ sở dữ liệu dưới dạng cấu trúc đồ thị.
  • Định nghĩa schema dưới dạng mã ngôn ngữ Go.
  • Thực hiện các loại tĩnh dựa trên việc tạo mã.
  • Việc viết truy vấn cơ sở dữ liệu và duyệt đồ thị rất đơn giản.
  • Dễ dàng mở rộng và tùy chỉnh bằng cách sử dụng Go templates.

2. Thiết lập Môi trường

Để bắt đầu sử dụng framework ent, hãy đảm bảo rằng ngôn ngữ Go đã được cài đặt trong môi trường phát triển của bạn.

Nếu thư mục dự án của bạn ở ngoài GOPATH, hoặc nếu bạn không quen thuộc với GOPATH, bạn có thể sử dụng lệnh sau để tạo một dự án Go module mới:

go mod init entdemo

Điều này sẽ khởi tạo một module Go mới và tạo một tệp go.mod mới cho dự án entdemo của bạn.

3. Xác định Schema Đầu tiên

3.1. Tạo Schema Bằng ent CLI

Đầu tiên, bạn cần chạy lệnh sau trong thư mục gốc của dự án của bạn để tạo schema có tên là User bằng công cụ ent CLI:

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

Lệnh trên sẽ tạo ra schema User trong thư mục entdemo/ent/schema/:

Tệp 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. Thêm Các Trường

Tiếp theo, chúng ta cần thêm định nghĩa trường vào Schema User. Dưới đây là một ví dụ về việc thêm hai trường vào entity User.

Tệp đã được sửa đổi 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"),
    }
}

Đoạn mã này định nghĩa hai trường cho mô hình User: agename, trong đó age là số nguyên dương và name là một chuỗi với giá trị mặc định là "unknown".

3.3. Tạo Thực thể Cơ sở dữ liệu

Sau khi xác định schema, bạn cần chạy lệnh go generate để tạo logic truy cập cơ sở dữ liệu cơ bản.

Chạy lệnh sau trong thư mục gốc của dự án của bạn:

go generate ./ent

Lệnh trên sẽ tạo ra mã Go tương ứng dựa trên schema đã xác định trước đó, dẫn đến cấu trúc tệp sau:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (một số tệp bị bỏ qua cho sự ngắn gọn)
├── 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. Khởi tạo Kết nối Cơ sở dữ liệu

Để thiết lập kết nối với cơ sở dữ liệu MySQL, chúng ta có thể sử dụng hàm Open được cung cấp bởi framework ent. Đầu tiên, nhập trình điều khiển MySQL sau đó cung cấp chuỗi kết nối chính xác để khởi tạo kết nối cơ sở dữ liệu.

package main

import (
    "context"
    "log"

    "entdemo/ent"
    
    _ "github.com/go-sql-driver/mysql" // Nhập trình điều khiển MySQL
)

func main() {
    // Sử dụng ent.Open để thiết lập kết nối với cơ sở dữ liệu MySQL.
    // Nhớ thay thế các giữ chỗ "your_username", "your_password", và "your_database" bên dưới.
    client, err := ent.Open("mysql", "your_username:your_password@tcp(localhost:3306)/your_database?parseTime=True")
    if err != nil {
        log.Fatalf("không thể mở kết nối đến mysql: %v", err)
    }
    defer client.Close()

    // Chạy công cụ di cư tự động
    ctx := context.Background()
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("không thể tạo tài nguyên schema: %v", err)
    }
    
    // Các logic kinh doanh bổ sung có thể được viết ở đây
}

4.2. Tạo Thực thể

Tạo một thực thể Người dùng liên quan đến việc xây dựng một đối tượng thực thể mới và lưu trữ nó vào cơ sở dữ liệu bằng cách sử dụng phương thức Save hoặc SaveX. Đoạn mã dưới đây minh họa cách tạo một thực thể Người dùng mới và khởi tạo hai trường agename.

// Hàm CreateUser được sử dụng để tạo một thực thể Người dùng mới
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Sử dụng client.User.Create() để xây dựng yêu cầu tạo một Người dùng,
    // sau đó chuỗi phương thức SetAge và SetName để đặt các giá trị của các trường thực thể.
    u, err := client.User.
        Create().
        SetAge(30).    // Đặt tuổi của người dùng
        SetName("a8m"). // Đặt tên của người dùng
        Save(ctx)     // Gọi Save để lưu thực thể vào cơ sở dữ liệu
    if err != nil {
        return nil, fmt.Errorf("không thể tạo người dùng: %w", err)
    }
    log.Println("người dùng đã được tạo: ", u)
    return u, nil
}

Trong hàm main, bạn có thể gọi hàm CreateUser để tạo một thực thể người dùng mới.

func main() {
    // ...Mã thiết lập kết nối cơ sở dữ liệu đã bỏ qua

    // Tạo một thực thể người dùng
    u, err := CreateUser(ctx, client)
    if err != nil {
        log.Fatalf("không thể tạo người dùng: %v", err)
    }
    log.Printf("đã tạo người dùng: %#v\n", u)
}

4.3. Truy vấn Thực thể

Để truy vấn các thực thể, chúng ta có thể sử dụng trình xây dựng truy vấn được tạo ra bởi ent. Đoạn mã dưới đây minh họa cách truy vấn một người dùng có tên "a8m":

// Hàm QueryUser được sử dụng để truy vấn thực thể người dùng với tên cụ thể
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // Sử dụng client.User.Query() để xây dựng truy vấn cho Người dùng,
    // sau đó chuỗi phương thức Where để thêm điều kiện truy vấn, chẳng hạn như truy vấn theo tên người dùng
    u, err := client.User.
        Query().
        Where(user.NameEQ("a8m")).      // Thêm điều kiện truy vấn, trong trường hợp này, tên là "a8m"
        Only(ctx)                      // Phương thức Only chỉ ra rằng chỉ có một kết quả được mong đợi
    if err != nil {
        return nil, fmt.Errorf("không thể truy vấn người dùng: %w", err)
    }
    log.Println("người dùng đã trả về: ", u)
    return u, nil
}

Trong hàm main, bạn có thể gọi hàm QueryUser để truy vấn thực thể người dùng.

func main() {
    // ...Mã thiết lập kết nối cơ sở dữ liệu và mã tạo người dùng đã bỏ qua

    // Truy vấn thực thể người dùng
    u, err := QueryUser(ctx, client)
    if err != nil {
        log.Fatalf("không thể truy vấn người dùng: %v", err)
    }
    log.Printf("đã truy vấn người dùng: %#v\n", u)
}

5.1. Hiểu Rõ Ràng Về Cạnh và Cạnh Nghịch Đảo

Trong framework ent, mô hình dữ liệu được biểu diễn như một cấu trúc đồ thị, trong đó các thực thể đại diện cho các nút trong đồ thị, và mối quan hệ giữa các thực thể được biểu diễn bởi các cạnh. Một cạnh là một kết nối từ một thực thể đến một thực thể khác, ví dụ, một User có thể sở hữu nhiều Cars.

Cạnh Nghịch Đảo là các tham chiếu đảo ngược đến cạnh, đại diện logic cho mối quan hệ đảo ngược giữa các thực thể, nhưng không tạo ra mối quan hệ mới trong cơ sở dữ liệu. Ví dụ, thông qua cạnh nghịch đảo của một Car, chúng ta có thể tìm thấy User sở hữu chiếc xe này.

Ý nghĩa chính của cạnh và cạnh nghịch đảo nằm ở việc làm cho việc điều hướng giữa các thực thể liên quan trở nên rất trực quan và dễ hiểu.

Mẹo: Trong ent, các cạnh tương ứng với các khóa ngoại của cơ sở dữ liệu truyền thống và được sử dụng để xác định mối quan hệ giữa các bảng.

5.2. Xác Định Cạnh trong Mô Hình

Trước hết, chúng ta sẽ sử dụng CLI của ent để tạo mô hình ban đầu cho CarGroup:

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

Tiếp theo, trong mô hình User, chúng ta xác định cạnh với Car để đại diện cho mối quan hệ giữa người dùng và xe. Chúng ta có thể thêm một cạnh cars trỏ đến kiểu Car trong thực thể người dùng, chỉ ra rằng một người dùng có thể có nhiều xe:

// entdemo/ent/schema/user.go

// Các cạnh của User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("cars", Car.Type),
    }
}

Sau khi xác định các cạnh, chúng ta cần chạy go generate ./ent một lần nữa để tạo mã tương ứng.

5.3. Thao Tác trên Dữ Liệu Cạnh

Việc tạo các xe liên kết với một người dùng là một quá trình đơn giản. Cho một thực thể người dùng, chúng ta có thể tạo một thực thể xe mới và liên kết nó với người dùng:

import (
    "context"
    "log"
    "entdemo/ent"
    // Đảm bảo import định nghĩa mô hình cho 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("thất bại khi lấy người dùng: %v", err)
        return err
    }

    // Tạo một chiếc xe mới và liên kết nó với người dùng
    _, err = client.Car.
        Create().
        SetModel("Tesla").
        SetRegisteredAt(time.Now()).
        SetOwner(user).
        Save(ctx)
    if err != nil {
        log.Fatalf("thất bại khi tạo xe cho người dùng: %v", err)
        return err
    }

    log.Println("xe đã được tạo và liên kết với người dùng")
    return nil
}

Tương tự, việc truy vấn xe của một người dùng là rất đơn giản. Nếu chúng ta muốn lấy danh sách tất cả các xe do một người dùng sở hữu, chúng ta có thể làm như sau:

func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("thất bại khi lấy người dùng: %v", err)
        return err
    }

    // Truy vấn tất cả các xe mà người dùng sở hữu
    cars, err := user.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("thất bại khi truy vấn xe: %v", err)
        return err
    }

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

Thông qua các bước trên, chúng ta không chỉ học cách xác định các cạnh trong mô hình mà còn thể hiện cách tạo và truy vấn dữ liệu liên quan đến các cạnh.

6. Truy Vấn và Điều Hướng Đồ Thị

6.1. Hiểu Cấu Trúc Đồ Thị

Trong ent, cấu trúc đồ thị được biểu diễn bởi các thực thể và các cạnh giữa chúng. Mỗi thực thể tương đương với một nút trong đồ thị, và mối quan hệ giữa các thực thể được biểu diễn bởi các cạnh, có thể là một-một, một-nhiều, nhiều-nhiều, v.v. Cấu trúc đồ thị này làm cho việc truy vấn phức tạp và thực hiện các thao tác trên cơ sở dữ liệu liên kết trở nên đơn giản và trực quan.

6.2. Duyệt Cấu Trúc Đồ Thị

Viết mã Duyệt Đồ Thị chủ yếu liên quan đến việc truy vấn và liên kết dữ liệu thông qua các cạnh giữa các thực thể. Dưới đây là một ví dụ đơn giản mô tả cách duyệt cấu trúc đồ thị trong ent:

import (
    "context"
    "log"

    "entdemo/ent"
)

// GraphTraversal là một ví dụ về việc duyệt cấu trúc đồ thị
func GraphTraversal(ctx context.Context, client *ent.Client) error {
    // Truy vấn người dùng tên "Ariel"
    a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
    if err != nil {
        log.Fatalf("Truy vấn người dùng thất bại: %v", err)
        return err
    }

    // Duyệt tất cả các xe thuộc sở hữu của Ariel
    cars, err := a8m.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("Truy vấn xe thất bại: %v", err)
        return err
    }
    for _, car := range cars {
        log.Printf("Ariel có một chiếc xe với model: %s", car.Model)
    }

    // Duyệt tất cả các nhóm mà Ariel là thành viên
    groups, err := a8m.QueryGroups().All(ctx)
    if err != nil {
        log.Fatalf("Truy vấn nhóm thất bại: %v", err)
        return err
    }
    for _, g := range groups {
        log.Printf("Ariel là thành viên của nhóm: %s", g.Name)
    }

    return nil
}

Mã trên là một ví dụ cơ bản về duyệt đồ thị, trước tiên truy vấn một người dùng và sau đó duyệt các xe và nhóm của người dùng đó.

7. Hiển Thị Schema Cơ Sở Dữ Liệu

7.1. Cài Đặt Công Cụ Atlas

Để hiển thị schema cơ sở dữ liệu được tạo ra bởi ent, chúng ta có thể sử dụng công cụ Atlas. Các bước cài đặt cho Atlas rất đơn giản. Ví dụ, trên macOS, bạn có thể cài đặt nó bằng brew:

brew install ariga/tap/atlas

Lưu ý: Atlas là một công cụ di dân cơ sở dữ liệu đa năng có thể xử lý việc quản lý phiên bản cấu trúc bảng cho các cơ sở dữ liệu khác nhau. Giới thiệu chi tiết về Atlas sẽ được cung cấp trong các chương sau.

7.2. Tạo ERD và Schema SQL

Sử dụng Atlas để xem và xuất schema rất đơn giản. Sau khi cài đặt Atlas, bạn có thể sử dụng lệnh sau để xem Biểu đồ Mối Quan Hệ Thực Thể (ERD):

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

Hoặc tạo trực tiếp Schema SQL:

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

Ở đây [database_dsn] trỏ đến tên nguồn dữ liệu (DSN) của cơ sở dữ liệu của bạn. Ví dụ, đối với cơ sở dữ liệu SQLite, nó có thể là:

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

Kết quả sinh ra bởi những lệnh này có thể được chuyển đổi thành các tầm nhìn hoặc tài liệu sử dụng các công cụ tương ứng.

8. Di Dân Schema

8.1. Di Dân Tự Động và Di Dân Phiên Bản

ent hỗ trợ hai chiến lược di dân schema: di dân tự động và di dân phiên bản. Di dân tự động là quá trình kiểm tra và áp dụng các thay đổi schema tại thời điểm chạy, phù hợp cho việc phát triển và kiểm thử. Di dân phiên bản liên quan đến việc tạo ra kịch bản di dân và yêu cầu xem xét cẩn thận và kiểm thử trước khi triển khai vào môi trường sản xuất.

Gợi ý: Đối với di dân tự động, hãy tham khảo nội dung trong phần 4.1.

8.2. Thực Hiện Di Dân Phiên Bản

Quá trình di dân phiên bản bao gồm tạo ra các tệp di dân thông qua Atlas. Dưới đây là các lệnh liên quan:

Để tạo ra các tệp di dân:

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

Sau đó, các tệp di dân này có thể được áp dụng vào cơ sở dữ liệu:

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

Sau quá trình này, bạn có thể duy trì một lịch sử di dân cơ sở dữ liệu trong hệ thống kiểm soát phiên bản và đảm bảo xem xét cẩn thận trước mỗi lần di dân.

Gợi ý: Tham khảo mã mẫu tại https://github.com/ent/ent/tree/master/examples/start