1. Giới thiệu về Hoạt động của Thực thể trong ent

Hướng dẫn này sẽ giúp bạn nắm vững về hoạt động thực thể trong framework ent, bao gồm toàn bộ quá trình tạo, truy vấn, cập nhật và xóa các thực thể. Được thiết kế phù hợp cho người mới bắt đầu dần dần khám phá chức năng cốt lõi của ent.

3. Hoạt động Tạo Thực thể

3.1 Tạo Một Thực thể Đơn

Việc tạo một thực thể là hoạt động cơ bản để lưu trữ dữ liệu. Dưới đây là các bước để tạo một đối tượng thực thể đơn sử dụng framework ent và lưu nó vào cơ sở dữ liệu:

  1. Đầu tiên, định nghĩa cấu trúc và trường của một thực thể, tức là định nghĩa mô hình của thực thể trong tệp schema.
  2. Chạy lệnh ent generate để tạo mã hoạt động thực thể tương ứng.
  3. Sử dụng phương thức Create được tạo ra để xây dựng một thực thể mới, và thiết lập giá trị trường của thực thể thông qua các cuộc gọi nối tiếp.
  4. Cuối cùng, gọi phương thức Save để lưu thực thể vào cơ sở dữ liệu.

Dưới đây là ví dụ minh họa cách tạo và lưu một thực thể người dùng:

package main

import (
    "context"
    "log"
    "entdemo/ent"
)

func main() {
    // Tạo một phiên bản Client để tương tác với cơ sở dữ liệu
    client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
    if err != nil {
        log.Fatalf("Không thể mở kết nối cơ sở dữ liệu: %v", err)
    }
    defer client.Close()

    // Tạo một ngữ cảnh
    ctx := context.Background()
    
    // Tạo một thực thể người dùng bằng cách sử dụng Client
    a8m, err := client.User.
        Create().
        SetName("a8m").
        Save(ctx)
    if err != nil {
        log.Fatalf("Không thể tạo thực thể người dùng: %v", err)
    }

    // Thực thể đã được lưu vào cơ sở dữ liệu thành công
    log.Printf("Thực thể người dùng đã được lưu: %v", a8m)
}

Trong ví dụ này, một phiên bản của cơ sở dữ liệu client được tạo trước tiên. Sau đó, phương thức User.Create được sử dụng để thiêt lập các thuộc tính của người dùng mới, và cuối cùng, phương thức Save được gọi để lưu người dùng vào cơ sở dữ liệu.

3.2 Tạo Thực thể theo Lô

Trong một số tình huống, có thể có nhu cầu tạo nhiều thực thể, chẳng hạn như trong quá trình khởi tạo cơ sở dữ liệu hoặc thao tác nhập dữ liệu hàng loạt. Framework ent cung cấp khả năng tạo thực thể theo lô, cung cấp hiệu suất tốt hơn so với việc tạo và lưu thực thể một cách riêng lẻ.

Các bước để tạo thực thể theo lô như sau:

  1. Sử dụng phương thức CreateBulk thay vì phương thức Create, cho phép tạo nhiều thực thể trong một hoạt động duy nhất.
  2. Gọi Create cho mỗi thực thể cần tạo.
  3. Sau khi tất cả thực thể đã được tạo, sử dụng phương thức Save để lưu các thực thể vào cơ sở dữ liệu theo lô.

Dưới đây là ví dụ về việc tạo thực thể theo lô:

package main

import (
    "context"
    "log"
    "entdemo/ent"
)

func main() {
    client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
    if err != nil {
        log.Fatalf("Không thể mở kết nối cơ sở dữ liệu: %v", err)
    }
    defer client.Close()
    ctx := context.Background()

    // Tạo thực thể Pet theo lô
    pets, err := client.Pet.CreateBulk(
        client.Pet.Create().SetName("pedro").SetOwner(a8m),
        client.Pet.Create().SetName("xabi").SetOwner(a8m),
        client.Pet.Create().SetName("layla").SetOwner(a8m),
    ).Save(ctx)
    if err != nil {
        log.Fatalf("Không thể tạo thực thể Pet theo lô: %v", err)
    }

    log.Printf("Đã tạo %d thực thể Pet trong lô\n", len(pets))
}

Trong ví dụ này, một client được tạo trước tiên, sau đó, nhiều thực thể Pet được xây dựng bằng cách sử dụng phương thức CreateBulk, thiết lập tên và trường chủ sở hữu của chúng. Tất cả các thực thể được lưu vào cơ sở dữ liệu cùng một lúc khi phương thức Save được gọi, cung cấp hiệu suất tốt cho việc xử lý lượng lớn dữ liệu.

4. Hoạt động Truy vấn Thực thể

4.1 Truy vấn cơ bản

Truy vấn cơ sở dữ liệu là cách cơ bản để truy xuất thông tin. Trong ent, phương thức Query được sử dụng để bắt đầu một truy vấn. Dưới đây là các bước và một ví dụ về việc truy vấn thực thể cơ bản:

  1. Hãy đảm bảo rằng bạn có một trường hợp Client có thể sử dụng.
  2. Sử dụng Client.Query() hoặc các phương thức trợ giúp của thực thể như Pet.Query() để tạo một truy vấn.
  3. Thêm các điều kiện lọc khi cần thiết, như Where.
  4. Thực hiện truy vấn và nhận kết quả bằng cách gọi phương thức All.
package main

import (
    "context"
    "log"
    "entdemo/ent"
    "entdemo/ent/user"
)

func main() {
    client, err := ent.Open("sqlite3", "file:ent?cache=shared&_fk=1")
    if err != nil {
        log.Fatalf("Không thể mở kết nối cơ sở dữ liệu: %v", err)
    }
    defer client.Close()
    ctx := context.Background()

    // Truy vấn tất cả người dùng có tên "a8m"
    users, err := client.User.
        Query().
        Where(user.NameEQ("a8m")).
        All(ctx)
    if err != nil {
        log.Fatalf("Không thể truy vấn người dùng: %v", err)
    }

    for _, u := range users {
        log.Printf("Tìm thấy người dùng: %#v\n", u)
    }
}

Ví dụ này minh họa cách tìm tất cả người dùng có tên "a8m".

4.2 Phân trang và Sắp xếp

Phân trang và sắp xếp là các tính năng nâng cao thường được sử dụng khi truy vấn, được sử dụng để kiểm soát thứ tự và số lượng dữ liệu đầu ra. Dưới đây là cách thực hiện truy vấn phân trang và sắp xếp bằng cách sử dụng ent:

  1. Sử dụng phương thức Limit để đặt số lượng kết quả tối đa cần trả về.
  2. Sử dụng phương thức Offset để bỏ qua một số kết quả trước đó.
  3. Sử dụng phương thức Order để chỉ định trường sắp xếp và hướng.

Dưới đây là một ví dụ về truy vấn phân trang và sắp xếp:

package main

import (
    "context"
    "log"
    "entdemo/ent"
    "entdemo/ent/pet"
)

func main() {
    client, err := ent.Open("sqlite3", "file:ent?cache=shared&_fk=1")
    if err != nil {
        log.Fatalf("Không thể mở kết nối cơ sở dữ liệu: %v", err)
    }
    defer client.Close()
    ctx := context.Background()

    // Truy vấn Pets theo thứ tự giảm dần của tuổi với phân trang
    pets, err := client.Pet.
        Query().
        Order(ent.Desc(pet.FieldAge)).
        Limit(10).
        Offset(0).
        All(ctx)
    if err != nil {
        log.Fatalf("Không thể truy vấn Pets: %v", err)
    }

    for _, p := range pets {
        log.Printf("Tìm thấy Pet: %#v\n", p)
    }
}

Ví dụ này minh họa cách lấy trang đầu tiên, tối đa 10 bản ghi của pets được sắp xếp theo tuổi giảm dần. Bằng cách thay đổi giá trị của LimitOffset, bạn có thể thực hiện phân trang qua toàn bộ tập dữ liệu.

5. Thao tác Cập nhật Thực thể

5.1 Cập nhật Một Thực thể Đơn

Trong nhiều ứng dụng, việc cập nhật các thực thể là một phần thiết yếu của các hoạt động hàng ngày. Ở phần này, chúng ta sẽ minh họa cách sử dụng khung công việc Ent để cập nhật một thực thể đơn trong cơ sở dữ liệu.

Đầu tiên, giả sử chúng ta cần cập nhật tuổi của một người dùng, chúng ta có thể sử dụng phương thức Update được tạo ra bởi Ent.

// Giả sử chúng ta đã có một thực thể người dùng 'a8m' và một context 'ctx'

a8m, err := a8m.Update().         // Tạo một bộ cập nhật người dùng
    SetAge(30).                   // Đặt tuổi của người dùng thành 30 tuổi
    Save(ctx)                     // Thực hiện hoạt động lưu và trả về kết quả
if err != nil {
    log.Fatalf("Không thể cập nhật người dùng: %v", err)
}

Bạn cũng có thể cập nhật nhiều trường đồng thời:

a8m, err := a8m.Update().
    SetAge(30).                    // Cập nhật tuổi
    SetName("Ariel").              // Cập nhật tên
    AddRank(10).                   // Tăng xếp hạng lên 10
    Save(ctx)
if err != nil {
    log.Fatalf("Không thể cập nhật người dùng: %v", err)
}

Các hoạt động cập nhật có thể được nối tiếp, rất linh hoạt và dễ đọc. Gọi phương thức Save sẽ thực hiện cập nhật và trả về thực thể đã cập nhật hoặc một thông báo lỗi.

5.2 Cập Nhật Có Điều Kiện

Ent cho phép bạn thực hiện cập nhật dữ liệu dựa trên điều kiện. Đây là một ví dụ trong đó chỉ những người dùng đáp ứng điều kiện cụ thể mới được cập nhật.

// Giả sử chúng ta có `id` của một người dùng và muốn đánh dấu người dùng đó là đã hoàn thành cho phiên bản `currentVersion`

err := client.Todo.
    UpdateOneID(id).          // Tạo một bộ xử lý để cập nhật theo ID người dùng
    SetStatus(todo.StatusDone).
    AddVersion(1).
    Where(
        todo.Version(currentVersion),  // Thao tác cập nhật chỉ được thực hiện khi phiên bản hiện tại khớp
    ).
    Exec(ctx)
switch {
case ent.IsNotFound(err):
    fmt.Println("Không tìm thấy công việc")
case err != nil:
    fmt.Println("Lỗi cập nhật:", err)
}

Khi sử dụng cập nhật có điều kiện, phương thức .Where() phải được sử dụng. Điều này cho phép bạn xác định liệu cập nhật có nên được thực hiện dựa trên các giá trị hiện tại trong cơ sở dữ liệu, điều quan trọng để đảm bảo tính nhất quán và toàn vẹn dữ liệu.

6. Thực Hiện Xóa Thực Thể

6.1 Xóa Một Thực Thể Duy Nhất

Xóa các thực thể là một chức năng quan trọng khác trong các hoạt động cơ sở dữ liệu. Framework Ent cung cấp một API đơn giản để thực hiện các hoạt động xóa.

Ví dụ dưới đây minh họa cách xóa một thực thể người dùng cụ thể:

err := client.User.
    DeleteOne(a8m).  // Tạo một bộ xử lý xóa người dùng
    Exec(ctx)        // Thực hiện thao tác xóa
if err != nil {
    log.Fatalf("Xóa người dùng thất bại: %v", err)
}

6.2 Xóa Có Điều Kiện

Tương tự như các thao tác cập nhật, chúng ta cũng có thể thực hiện các hoạt động xóa dựa trên điều kiện cụ thể. Trong một số tình huống, chúng ta có thể chỉ muốn xóa các thực thể đáp ứng điều kiện cụ thể. Sử dụng phương thức .Where() có thể định nghĩa các điều kiện này:

// Giả sử chúng ta muốn xóa tất cả các tệp có thời gian cập nhật trước một ngày nhất định

affected, err := client.File.
    Delete().
    Where(file.UpdatedAtLT(date)).  // Chỉ thực hiện xóa nếu thời gian cập nhật của tệp sớm hơn ngày đã cho
    Exec(ctx)
if err != nil {
    log.Fatalf("Xóa tệp thất bại: %v", err)
}

// Thao tác này trả về số lượng bản ghi bị ảnh hưởng bởi thao tác xóa
fmt.Printf("%d tệp đã được xóa\n", affected)

Sử dụng các hoạt động xóa có điều kiện đảm bảo việc kiểm soát chính xác các hoạt động dữ liệu của chúng ta, đảm bảo rằng chỉ có các thực thể thực sự đáp ứng điều kiện mới bị xóa, từ đó tăng cường tính bảo mật và đáng tin cậy của các hoạt động cơ sở dữ liệu.