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: age
và name
, 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 age
và name
.
// 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 Car
và Group
:
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