1. Cài đặt công cụ ent

Để cài đặt công cụ sinh mã ent, bạn cần tuân theo các bước sau:

Đầu tiên, hãy đảm bảo rằng hệ thống của bạn đã cài đặt môi trường ngôn ngữ Go. Sau đó, chạy lệnh sau để lấy công cụ ent:

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

Lệnh này sẽ tải mã nguồn cho công cụ ent, nhưng nó sẽ không biên dịch và cài đặt ngay lập tức. Nếu bạn muốn cài đặt ent vào thư mục $GOPATH/bin để bạn có thể sử dụng nó ở bất kỳ đâu, bạn cũng cần thực hiện lệnh sau:

go install entgo.io/ent/cmd/ent

Sau khi cài đặt hoàn tất, bạn có thể kiểm tra xem công cụ ent đã được cài đặt đúng cách và xem các lệnh và hướng dẫn có sẵn bằng cách chạy ent -h.

2. Khởi tạo Schema

2.1 Khởi tạo Mẫu Sử Dụng ent init

Tạo một tệp schema mới là bước đầu tiên để bắt đầu sử dụng ent để sinh mã. Bạn có thể khởi tạo mẫu schema bằng cách thực hiện lệnh sau:

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

Lệnh này sẽ tạo hai tệp schema mới: user.gopet.go, và đặt chúng trong thư mục ent/schema. Nếu thư mục ent chưa tồn tại, lệnh này cũng sẽ tự động tạo nó.

Chạy lệnh ent init trong thư mục gốc dự án là một thói quen tốt, vì nó giúp duy trì cấu trúc và rõ ràng của thư mục dự án.

2.2 Cấu Trúc Tệp Schema

Trong thư mục ent/schema, mỗi schema tương ứng với một tệp nguồn ngôn ngữ Go. Tệp schema là nơi bạn định nghĩa mô hình cơ sở dữ liệu, bao gồm các trường và cạnh (mối quan hệ).

Ví dụ, trong tệp user.go, bạn có thể định nghĩa mô hình người dùng, bao gồm các trường như tên người dùng và tuổi, và định nghĩa mối quan hệ giữa người dùng và vật nuôi. Tương tự, trong tệp pet.go, bạn sẽ định nghĩa mô hình vật nuôi và các trường liên quan, như tên vật nuôi, loại và mối quan hệ giữa vật nuôi và người dùng.

Những tệp này cuối cùng sẽ được sử dụng bởi công cụ ent để sinh mã Go tương ứng, bao gồm mã khách hàng cho các hoạt động cơ sở dữ liệu và các hoạt động CRUD (Tạo, Đọc, Cập nhật, Xóa).

// ent/schema/user.go
package schema

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

// User định nghĩa schema cho thực thể Người dùng.
type User struct {
    ent.Schema
}

// Phương thức Fields được sử dụng để định nghĩa các trường của thực thể.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").Unique(),
        field.Int("age").Positive(),
    }
}

// Phương thức Edges được sử dụng để định nghĩa các liên kết của thực thể.
func (User) Edges() []ent.Edge {
    // Các liên kết sẽ được giải thích chi tiết hơn ở phần tiếp theo.
}

Tệp schema của ent sử dụng các loại và hàm ngôn ngữ Go để khai báo cấu trúc của mô hình cơ sở dữ liệu, bao gồm các trường và mối quan hệ giữa các mô hình, và sử dụng API được cung cấp bởi framework ent để định nghĩa những cấu trúc này. Phương pháp này làm cho ent rất trực quan và dễ mở rộng, đồng thời cũng tận dụng được kiểu dữ liệu mạnh mẽ của ngôn ngữ Go.

3.1 Chạy Sinh Mã

Chạy ent generate để tạo mã là một bước quan trọng trong framework ent. Với lệnh này, ent sẽ tạo các tệp mã Go tương ứng dựa trên các schema đã định nghĩa, giúp cho công việc phát triển sau này trở nên dễ dàng hơn. Lệnh để thực hiện sinh mã rất đơn giản:

go generate ./ent

Lệnh trên cần phải được chạy trong thư mục gốc dự án. Khi gọi go generate, nó sẽ tìm kiếm tất cả các tệp Go chứa các chú thích cụ thể và thực hiện các lệnh được chỉ định trong các chú thích. Trong ví dụ của chúng ta, lệnh này chỉ định trình tạo mã cho ent.

Đảm bảo rằng việc khởi tạo schema và việc thêm trường cần thiết đã hoàn thành trước khi thực hiện. Chỉ khi đó mã sinh ra mới bao gồm tất cả các phần cần thiết.

3.2 Hiểu về Các Tài Nguyên Mã Sinh Ra

Các tài nguyên mã sinh ra chứa nhiều thành phần khác nhau, mỗi thành phần có chức năng khác nhau:

  • Đối tượng Client và Tx: Được sử dụng để tương tác với đồ thị dữ liệu. Client cung cấp các phương thức để tạo giao dịch (Tx) hoặc thực hiện trực tiếp các thao tác cơ sở dữ liệu.

  • Công cụ CRUD builders: Đối với mỗi loại schema, nó tạo ra các công cụ để tạo, đọc, cập nhật, và xóa, đơn giản hóa logic thao tác của thực thể tương ứng.

  • Đối tượng Entity (Go struct): Nó tạo ra các struct Go tương ứng cho mỗi loại trong schema, ánh xạ các struct này vào các bảng trong cơ sở dữ liệu.

  • Hằng số và gói điều kiện (constants and predicates package): Chứa các hằng số và điều kiện để tương tác với các công cụ tạo.

  • Gói Migrate: Một gói để di chuyển cơ sở dữ liệu, phù hợp với các ngôn ngữ SQL.

  • Gói Hook: Cung cấp chức năng để thêm middleware thay đổi, cho phép logic tùy chỉnh được thực thi trước hoặc sau khi tạo, cập nhật, hoặc xóa các thực thể.

Bằng cách xem xét mã sinh ra, bạn có thể hiểu sâu hơn về cách ent tự động hóa mã truy cập dữ liệu cho các schema của bạn.

4. Tùy Chỉnh Tùy Chọn Mã Sinh Ra

Lệnh ent generate hỗ trợ các tùy chọn khác nhau để tùy chỉnh quá trình mã sinh ra. Bạn có thể truy vấn tất cả các tùy chọn mã sinh ra được hỗ trợ thông qua lệnh sau:

ent generate -h

Dưới đây là một số tùy chọn dòng lệnh thường được sử dụng:

  • --feature strings: Mở rộng mã sinh ra, thêm chức năng bổ sung.
  • --header string: Ghi đè tệp tiêu đề mã sinh ra.
  • --storage string: Chỉ định trình điều khiển lưu trữ được hỗ trợ trong quá trình sinh mã, mặc định là "sql".
  • --target string: Chỉ định thư mục đích cho mã sinh ra.
  • --template strings: Thực thi các bản mẫu Go bổ sung. Hỗ trợ tệp, thư mục, và đường dẫn đại diện, ví dụ: --template file="đường/dẫn/đến/tệp.tmpl".

Những tùy chọn này cho phép các nhà phát triển tùy chỉnh quá trình sinh mã của họ theo nhu cầu và sở thích khác nhau.

5. Cấu Hình Tùy Chọn Lưu Trữ

ent hỗ trợ việc sinh mã cho cả các ngôn ngữ SQL và Gremlin, với mặc định là ngôn ngữ SQL. Nếu dự án cần kết nối với cơ sở dữ liệu Gremlin, ngôn ngữ lưu trữ tương ứng cần được cấu hình. Dưới đây là cách chỉ định tùy chọn lưu trữ:

ent generate --storage gremlin ./ent/schema

Lệnh trên chỉ dẫn ent sử dụng ngôn ngữ lưu trữ Gremlin khi sinh mã. Điều này đảm bảo rằng các tài nguyên mã sinh ra được tùy chỉnh theo yêu cầu của cơ sở dữ liệu Gremlin cụ thể, đảm bảo tính tương thích với một cơ sở dữ liệu đồ thị cụ thể.

6. Sử Dụng Nâng Cao: Gói entc

6.1 Sử Dụng Package entc trong Dự Án

entc là gói lõi được sử dụng cho việc sinh mã trong framework ent. Ngoài công cụ dòng lệnh, entc cũng có thể được tích hợp vào dự án dưới dạng gói, cho phép các nhà phát triển kiểm soát và tùy chỉnh quá trình sinh mã trong mã nguồn của dự án.

Để sử dụng entc như một gói trong dự án, bạn cần tạo một tệp có tên entc.go và thêm nội dung sau vào tệp:

// +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)
    }
}

Khi sử dụng cách tiếp cận này, bạn có thể sửa đổi cấu trúc gen.Config trong hàm main để áp dụng các tùy chọn cấu hình khác nhau. Bằng cách gọi hàm entc.Generate khi cần thiết, bạn có thể linh hoạt kiểm soát quá trình sinh mã.

6.2 Cấu hình Chi tiết của entc

entc cung cấp nhiều tùy chọn cấu hình mở rộng, cho phép các nhà phát triển tùy chỉnh mã được tạo ra. Ví dụ, các hook tùy chỉnh có thể được cấu hình để kiểm tra hoặc sửa đổi mã được tạo ra, và các phụ thuộc bên ngoài có thể được tiêm vào bằng cách sử dụng dependency injection.

Ví dụ dưới đây minh họa cách cung cấp các hook tùy chỉnh cho hàm entc.Generate:

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

// HookFunction là một hàm hook tùy chỉnh
func HookFunction(next gen.Generator) gen.Generator {
    return gen.GenerateFunc(func(g *gen.Graph) error {
        // Có thể xử lý chế độ đồ thị được biểu diễn bởi g ở đây
        // Ví dụ, xác thực sự tồn tại của trường hoặc sửa đổi cấu trúc
        return next.Generate(g)
    })
}

Ngoài ra, các phụ thuộc bên ngoài có thể được thêm vào bằng cách sử dụng 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)
    }
}

Trong ví dụ này, chúng ta tiêm http.Clientio.Writer như là các phụ thuộc vào các đối tượng client được tạo ra.

7. Đầu ra của Mô tả Schema

Trong framework ent, lệnh ent describe có thể được sử dụng để xuất ra mô tả của schema dưới dạng đồ họa. Điều này có thể giúp các nhà phát triển nhanh chóng hiểu về các thực thể và mối quan hệ hiện có.

Thực hiện lệnh sau để có được mô tả của schema:

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

Lệnh trên sẽ xuất ra một bảng tương tự như sau, hiển thị thông tin như các trường, kiểu dữ liệu, mối quan hệ, v.v. cho mỗi thực thể:

User:
    +-------+---------+--------+-----------+ ...
    | Trường |  Kiểu   | Unique | Optional  | ...
    +-------+---------+--------+-----------+ ...
    | id    | int     | false  | false     | ...
    | name  | string  | true   | false     | ...
    +-------+---------+--------+-----------+ ...
    +-------+--------+---------+-----------+ ...
    | Edge  |  kiểu  | Nghịch đảo | Mối quan hệ  | ...
    +-------+--------+---------+-----------+ ...
    | pets  | Pet    | false   | O2M       | ...
    +-------+--------+---------+-----------+ ...

8. Hook tạo mã

8.1 Khái niệm về Hooks

Hooks là các hàm middleware mà có thể được chèn vào quá trình tạo mã của ent, cho phép logic tùy chỉnh được chèn vào trước và sau khi tạo mã. Hooks có thể được sử dụng để thao tác với cấu trúc cú pháp trừu tượng (AST) của mã được tạo ra, thực hiện xác thực, hoặc thêm các đoạn mã bổ sung.

8.2 Ví dụ về Việc Sử Dụng Hooks

Dưới đây là một ví dụ về việc sử dụng hook để đảm bảo rằng tất cả các trường chứa một struct tag nhất định (ví dụ, 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 đảm bảo rằng tất cả các trường trong đồ thị chứa một struct tag cụ thể
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)
        })
    }
}

Trong ví dụ này, trước khi tạo mã, hàm EnsureStructTag kiểm tra từng trường để xem liệu trường đó có tag json hay không. Nếu một trường thiếu tag này, quá trình tạo mã sẽ dừng lại và trả về lỗi. Điều này là một cách hiệu quả để duy trì tính sạch sẽ và nhất quán của mã.