Bun ORM là gì? Hướng dẫn sử dụng Bun ORM với Go và PostgreSQL

1. Bun ORM là gì?

Bun là một thư viện ORM dành cho ngôn ngữ Go, được thiết kế theo hướng SQL-first. Thay vì cố gắng che giấu hoàn toàn SQL phía sau các phương thức trừu tượng, Bun cho phép lập trình viên xây dựng truy vấn bằng Go trong khi vẫn giữ được cấu trúc và cách tư duy gần với SQL.

ORM (Object-Relational Mapping) là kỹ thuật ánh xạ giữa các cấu trúc dữ liệu trong chương trình và các đối tượng trong cơ sở dữ liệu quan hệ. ORM giúp lập trình viên thao tác với dữ liệu thông qua các model và API của thư viện, đồng thời giảm bớt một số công việc lặp lại như xây dựng truy vấn và ánh xạ kết quả vào struct.

Ví dụ, thay vì tự viết câu lệnh SQL để truy vấn sản phẩm:

SELECT id, name, price
FROM products
WHERE price > 100;

Có thể sử dụng Bun để xây dựng truy vấn tương ứng bằng Go:

var products []Product

err := db.NewSelect().
    Model(&products).
    Where("price > ?", 100).
    Scan(ctx)

Bun xây dựng và thực thi truy vấn SQL, sau đó ánh xạ kết quả vào biến products. Nếu truy vấn thất bại, biến err sẽ nhận thông tin lỗi.

Bun nằm ở đâu trong kiến trúc Backend Go?

Trong một ứng dụng Backend sử dụng Gin và PostgreSQL, Bun thường được sử dụng ở tầng truy cập dữ liệu (Data Access Layer).

Một kiến trúc phổ biến có thể được tổ chức như sau:

Gin Handler → Service → Repository → Bun ORM → PostgreSQL

  • Gin Handler: tiếp nhận HTTP request và trả về HTTP response.
  • Service: xử lý nghiệp vụ và điều phối các thao tác cần thiết.
  • Repository: đóng gói các thao tác truy vấn và lưu trữ dữ liệu.
  • Bun ORM: hỗ trợ xây dựng, thực thi truy vấn SQL và ánh xạ dữ liệu.
  • PostgreSQL: lưu trữ và xử lý dữ liệu.

Đây là một cách tổ chức phổ biến, không phải cấu trúc bắt buộc. Với ứng dụng nhỏ, có thể không cần tách riêng Repository thành một tầng độc lập.

Bun không thay thế Gin, không trực tiếp xử lý HTTP request và cũng không thay thế PostgreSQL. Mỗi thành phần đảm nhận một trách nhiệm khác nhau.

2. Bun hoạt động như thế nào?

Để hiểu cách Bun hoạt động, cần nắm mối quan hệ giữa model Go, truy vấn SQL và database.

Model Go

Model là struct dùng để biểu diễn dữ liệu trong chương trình. Bun có thể sử dụng các trường và struct tag để xác định cách ánh xạ dữ liệu với bảng và cột trong database.

Ví dụ:

type Product struct {
    ID    int64   `bun:"id,pk,autoincrement"`
    Name  string  `bun:"name,notnull"`
    Price float64 `bun:"price,notnull"`
}

func (Product) TableName() string {
    return "products"
}

Trong ví dụ này:

  • Product là struct đại diện cho dữ liệu sản phẩm.
  • ID, Name và Price là các trường dữ liệu.
  • bun:"id,pk,autoincrement" khai báo các thuộc tính ánh xạ cho trường ID.
  • TableName() chỉ định tên bảng là products.

Model không tự tạo ra bảng trong PostgreSQL. Nó mô tả cách Bun ánh xạ dữ liệu; bảng phải tồn tại hoặc được tạo thông qua migration hay câu lệnh SQL phù hợp.

Truy vấn SQL

Khi lập trình viên xây dựng truy vấn bằng API của Bun, thư viện sẽ tạo câu lệnh SQL tương ứng và sử dụng kết nối database để thực thi.

Ví dụ:

var products []Product

err := db.NewSelect().
    Model(&products).
    Where("price > ?", 100).
    Order("price ASC").
    Scan(ctx)

Truy vấn này tương ứng về mặt ý nghĩa với SQL:

SELECT id, name, price
FROM products
WHERE price > 100
ORDER BY price ASC;

Đây là cách biểu diễn SQL tương đương để minh họa; SQL thực tế do Bun tạo ra có thể khác về định dạng, dấu ngoặc kép hoặc cách sử dụng tham số.

Quá trình xử lý có thể tóm tắt như sau:

  1. Lập trình viên định nghĩa model và xây dựng truy vấn bằng Bun.
  2. Bun tạo câu lệnh SQL cùng các tham số cần thiết.
  3. Truy vấn được gửi đến PostgreSQL thông qua kết nối database.
  4. PostgreSQL thực thi truy vấn và trả về kết quả.
  5. Bun ánh xạ dữ liệu nhận được vào struct hoặc slice tương ứng.

Như vậy, Bun không tự thực hiện việc truy vấn dữ liệu thay cho database. PostgreSQL vẫn chịu trách nhiệm thực thi SQL, còn Bun hỗ trợ việc xây dựng truy vấn và xử lý dữ liệu ở phía Go.

3. Cài đặt và kết nối PostgreSQL

Cài đặt thư viện

Bun sử dụng database/sql của Go để làm việc với kết nối database. Để sử dụng Bun với PostgreSQL, cần cài thư viện Bun, PostgreSQL driver và dialect tương ứng.

Chạy các lệnh sau trong thư mục dự án Go:

go get github.com/uptrace/bun
go get github.com/uptrace/bun/driver/pgdriver
go get github.com/uptrace/bun/dialect/pgdialect

Trong đó:

  • github.com/uptrace/bun: thư viện ORM chính.
  • github.com/uptrace/bun/driver/pgdriver: driver kết nối PostgreSQL dành cho Bun.
  • github.com/uptrace/bun/dialect/pgdialect: cung cấp thông tin về dialect PostgreSQL để Bun xây dựng truy vấn phù hợp.

Kết nối database

Ví dụ dưới đây minh họa cách tạo kết nối PostgreSQL và khởi tạo Bun:

package main

import (
    "database/sql"
    "log"

    "github.com/uptrace/bun"
    "github.com/uptrace/bun/dialect/pgdialect"
    "github.com/uptrace/bun/driver/pgdriver"
)

func main() {
    dsn := "postgres://postgres:your_password@localhost:5432/shop_db?sslmode=disable"

    sqldb := sql.OpenDB(pgdriver.NewConnector(
        pgdriver.WithDSN(dsn),
    ))

    db := bun.NewDB(sqldb, pgdialect.New())
    defer db.Close()

    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }

    log.Println("Kết nối database thành công")
}

Ví dụ sử dụng API của Bun với PostgreSQL driver chính thức trong hệ sinh thái Bun. Mật khẩu, tên database và các tham số kết nối cần được thay bằng thông tin phù hợp với môi trường của bạn.

DSN là gì?

DSN (Data Source Name) là chuỗi chứa thông tin cần thiết để thiết lập kết nối tới database.

Ví dụ:

postgres://postgres:your_password@localhost:5432/shop_db?sslmode=disable

Các thành phần gồm:

  • postgres://: scheme cho biết loại kết nối.
  • postgres: tên người dùng database.
  • your_password: mật khẩu.
  • localhost: địa chỉ máy chủ database.
  • 5432: cổng PostgreSQL mặc định.
  • shop_db: tên database cần kết nối.
  • sslmode=disable: tắt TLS cho kết nối trong ví dụ này; chỉ phù hợp với môi trường cục bộ được kiểm soát, không nên mặc định sử dụng trong môi trường production.

sql.OpenDB() tạo đối tượng kết nối dựa trên connector. Việc gọi Ping() giúp kiểm tra khả năng kết nối tới database tại thời điểm thực thi.

Trong ứng dụng thực tế, nên lấy thông tin kết nối từ biến môi trường hoặc hệ thống quản lý secrets thay vì ghi trực tiếp mật khẩu vào mã nguồn.

4. Model và CRUD

CRUD là viết tắt của Create, Read, Update và Delete — bốn thao tác cơ bản khi làm việc với dữ liệu.

Các ví dụ dưới đây sử dụng model Product đã khai báo ở phần trước và giả định bảng products đã tồn tại trong PostgreSQL.

4.1. Create — Tạo dữ liệu

Để thêm một sản phẩm mới:

product := &Product{
    Name:  "Keyboard",
    Price: 500000,
}

_, err := db.NewInsert().
    Model(product).
    Exec(ctx)

if err != nil {
    log.Fatal(err)
}

NewInsert() tạo truy vấn INSERT, Model(product) cung cấp dữ liệu cần ghi và Exec(ctx) thực thi truy vấn.

Nếu cột id được PostgreSQL tự sinh, giá trị ID có thể được trả về hoặc cập nhật vào model tùy cấu hình và cách sử dụng. Khi cần lấy dữ liệu được tạo một cách rõ ràng, có thể sử dụng Returning().

4.2. Read — Truy vấn dữ liệu

Để lấy một sản phẩm theo ID:

var product Product

err := db.NewSelect().
    Model(&product).
    Where("id = ?", 1).
    Limit(1).
    Scan(ctx)

if err != nil {
    log.Fatal(err)
}

Để lấy nhiều sản phẩm:

var products []Product

err := db.NewSelect().
    Model(&products).
    Scan(ctx)

if err != nil {
    log.Fatal(err)
}

Scan(ctx) thực thi truy vấn và đưa kết quả vào biến được chỉ định. Với truy vấn nhiều bản ghi, biến nhận kết quả thường là một slice.

Cần xử lý trường hợp truy vấn không trả về dữ liệu và các lỗi database theo cách phù hợp với nghiệp vụ của ứng dụng.

4.3. Update — Cập nhật dữ liệu

Để cập nhật giá của sản phẩm có ID bằng 1:

_, err := db.NewUpdate().
    Model((*Product)(nil)).
    Set("price = ?", 550000).
    Where("id = ?", 1).
    Exec(ctx)

if err != nil {
    log.Fatal(err)
}

NewUpdate() tạo truy vấn UPDATE, Set() chỉ định giá trị cần cập nhật và Where() xác định bản ghi cần tác động.

Điều kiện WHERE rất quan trọng. Nếu bỏ điều kiện này, truy vấn có thể cập nhật toàn bộ bản ghi trong bảng.

4.4. Delete — Xóa dữ liệu

Để xóa sản phẩm có ID bằng 1:

_, err := db.NewDelete().
    Model((*Product)(nil)).
    Where("id = ?", 1).
    Exec(ctx)

if err != nil {
    log.Fatal(err)
}

Bun xây dựng truy vấn DELETE tương ứng và thực thi trên database.

Cũng như UPDATE, cần xác định điều kiện xóa rõ ràng để tránh tác động ngoài ý muốn. Với dữ liệu cần lưu lịch sử, ứng dụng có thể thiết kế cơ chế soft delete thay vì xóa vật lý.

5. Truy vấn nâng cao

Ngoài các thao tác CRUD cơ bản, Bun hỗ trợ xây dựng những truy vấn phức tạp hơn bằng cách kết hợp các thành phần truy vấn.

5.1. WHERE — Lọc dữ liệu

Ví dụ lấy các sản phẩm có giá từ 100.000 trở lên:

var products []Product

err := db.NewSelect().
    Model(&products).
    Where("price >= ?", 100000).
    Scan(ctx)

Dấu ? là vị trí tham số trong API truy vấn của Bun. Giá trị được truyền riêng qua API, thay vì ghép trực tiếp vào chuỗi SQL.

Cách truyền tham số giúp tránh việc nối chuỗi dữ liệu đầu vào không an toàn, từ đó giảm nguy cơ SQL injection. Tuy nhiên, tên cột hoặc đoạn SQL động vẫn cần được kiểm soát và xác thực phù hợp.

5.2. JOIN — Kết hợp dữ liệu từ nhiều bảng

Giả sử hệ thống có hai bảng:

  • products: chứa thông tin sản phẩm.
  • categories: chứa thông tin danh mục.

Mỗi sản phẩm có một category_id tham chiếu đến danh mục tương ứng.

Có thể khai báo model:

type Category struct {
    ID   int64  `bun:"id,pk,autoincrement"`
    Name string `bun:"name,notnull"`
}

func (Category) TableName() string {
    return "categories"
}

type ProductWithCategory struct {
    ID           int64   `bun:"id"`
    Name         string  `bun:"name"`
    Price        float64 `bun:"price"`
    CategoryName string  `bun:"category_name"`
}

func (ProductWithCategory) TableName() string {
    return "products"
}

Sau đó sử dụng JOIN để lấy tên danh mục cùng sản phẩm:

var products []ProductWithCategory

err := db.NewSelect().
    Model(&products).
    ColumnExpr("product.id, product.name, product.price").
    ColumnExpr("category.name AS category_name").
    TableExpr("products AS product").
    Join("LEFT JOIN categories AS category ON category.id = product.category_id").
    Scan(ctx)

Ví dụ minh họa cách sử dụng JOIN trong Bun. Tùy cách khai báo model và phiên bản thư viện, cần bảo đảm tên bảng, bí danh và cách ánh xạ cột khớp với cấu trúc database thực tế.

Về bản chất, JOIN vẫn được PostgreSQL thực thi. Bun giúp xây dựng truy vấn và ánh xạ kết quả vào struct.

5.3. Quan hệ giữa các model

Bun hỗ trợ khai báo các quan hệ dữ liệu như belongs-to, has-one, has-many và many-to-many.

Ví dụ, một sản phẩm thuộc về một danh mục:

type Product struct {
    ID         int64    `bun:"id,pk,autoincrement"`
    Name       string   `bun:"name,notnull"`
    CategoryID int64    `bun:"category_id"`
    Category   *Category `bun:"rel:belongs-to,join:category_id=id"`
}

func (Product) TableName() string {
    return "products"
}

Sau đó có thể truy vấn sản phẩm cùng thông tin danh mục thông qua quan hệ đã khai báo:

var products []Product

err := db.NewSelect().
    Model(&products).
    Relation("Category").
    Scan(ctx)

Relation("Category") yêu cầu Bun truy vấn quan hệ đã khai báo. Bun có thể xây dựng JOIN phù hợp và ánh xạ dữ liệu quan hệ vào trường Category.

Khai báo quan hệ trong model không tự tạo ra khóa ngoại trong database. Các ràng buộc dữ liệu cần được thiết lập thông qua SQL hoặc migration.

5.4. Phân trang — Pagination

Khi bảng có nhiều bản ghi, việc trả về toàn bộ dữ liệu có thể làm tăng thời gian xử lý và lượng dữ liệu truyền tải.

Có thể sử dụng Limit() và Offset() để phân trang:

page := 2
pageSize := 10
offset := (page - 1) * pageSize

var products []Product

err := db.NewSelect().
    Model(&products).
    Order("id ASC").
    Limit(pageSize).
    Offset(offset).
    Scan(ctx)

Trong ví dụ này:

  • Limit(10) giới hạn mỗi trang tối đa 10 bản ghi.
  • Offset(10) bỏ qua 10 bản ghi đầu tiên.
  • Order("id ASC") tạo thứ tự ổn định hơn khi phân trang.

Với các bảng lớn hoặc dữ liệu thường xuyên thay đổi, keyset pagination có thể phù hợp hơn OFFSET vì không phải bỏ qua quá nhiều bản ghi ở các trang sâu.

5.5. Transaction — Giao dịch

Transaction cho phép nhóm nhiều thao tác database thành một đơn vị xử lý. Nếu tất cả thao tác thành công, transaction được commit; nếu xảy ra lỗi, các thay đổi trong transaction có thể được rollback.

Ví dụ:

err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
    product := &Product{
        Name:  "Mouse",
        Price: 200000,
    }

    if _, err := tx.NewInsert().
        Model(product).
        Exec(ctx); err != nil {
        return err
    }

    if _, err := tx.NewUpdate().
        Model((*Product)(nil)).
        Set("price = ?", 220000).
        Where("id = ?", 1).
        Exec(ctx); err != nil {
        return err
    }

    return nil
})

if err != nil {
    log.Fatal(err)
}

Trong ví dụ này, cả thao tác thêm sản phẩm và cập nhật giá đều nằm trong cùng transaction. Nếu callback trả về lỗi, Bun thực hiện rollback; nếu callback hoàn tất thành công, transaction được commit.

Transaction đặc biệt hữu ích khi nhiều thao tác có quan hệ phụ thuộc với nhau. Tuy nhiên, nó không tự giải quyết mọi vấn đề đồng thời; việc thiết kế transaction, khóa và mức cô lập vẫn cần được cân nhắc.

6. Ưu điểm, hạn chế và so sánh Bun với GORM

Ưu điểm của Bun

  • SQL-first: cách viết truy vấn gần với SQL, thuận tiện khi cần hiểu rõ database đang làm gì.
  • Linh hoạt: hỗ trợ xây dựng truy vấn với điều kiện, JOIN, subquery và các biểu thức SQL.
  • Ánh xạ dữ liệu: hỗ trợ ánh xạ kết quả truy vấn vào struct, slice và các kiểu dữ liệu phù hợp.
  • Hỗ trợ quan hệ và transaction: giúp tổ chức các thao tác truy vấn dữ liệu và xử lý nhiều thao tác trong một giao dịch.
  • Tích hợp với database/sql: tận dụng hệ sinh thái kết nối database của Go.

Hạn chế của Bun

  • Lập trình viên cần hiểu SQL tương đối tốt để khai thác hiệu quả các truy vấn phức tạp.
  • Không phải mọi thao tác đều được trừu tượng hóa thành một phương thức đơn giản; đôi khi vẫn cần viết biểu thức SQL.
  • Cần hiểu rõ cấu trúc database và quản lý migration, chỉ mục, ràng buộc cũng như transaction.
  • Việc dùng ORM không bảo đảm truy vấn luôn nhanh hoặc tránh được các vấn đề như N+1 query.

So sánh Bun và GORM

Tiêu chí Bun GORM
Triết lý SQL-first, giữ cách tư duy gần với SQL ORM cung cấp nhiều API thao tác model
Xây dựng truy vấn NewSelect(), Where(), Scan() Find(), First(), Where()
Model Struct kết hợp với Bun tags Struct kết hợp với GORM tags
Migration Có công cụ migration riêng Có AutoMigrate() và hỗ trợ migration
Quan hệ dữ liệu Khai báo quan hệ và sử dụng Relation() Associations và Preload()
Cách tiếp cận Phù hợp khi muốn kiểm soát cấu trúc truy vấn rõ ràng Phù hợp khi muốn dùng các API ORM phổ biến và trực quan

Không thể kết luận thư viện nào nhanh hơn trong mọi tình huống chỉ dựa trên đặc điểm thiết kế. Hiệu năng thực tế còn phụ thuộc vào SQL được tạo ra, chỉ mục, lượng dữ liệu, cách sử dụng connection pool và đặc điểm tải của ứng dụng.

Khi nào nên sử dụng Bun?

Bun có thể phù hợp khi:

  • Bạn phát triển Backend bằng Go và sử dụng PostgreSQL hoặc hệ quản trị được Bun hỗ trợ.
  • Bạn muốn viết truy vấn bằng Go nhưng vẫn duy trì tư duy SQL rõ ràng.
  • Ứng dụng có các truy vấn cần JOIN, lọc dữ liệu hoặc xây dựng điều kiện linh hoạt.
  • Bạn muốn sử dụng các công cụ của Go như database/sql cùng API truy vấn của Bun.

GORM có thể phù hợp khi bạn muốn sử dụng hệ sinh thái ORM phổ biến, các API thao tác model trực quan và cơ chế AutoMigrate() cho một số thay đổi schema trong quá trình phát triển.

Dù chọn thư viện nào, việc hiểu SQL, thiết kế database, chỉ mục, transaction và xử lý lỗi vẫn là nền tảng quan trọng khi xây dựng Backend.

Kết luận

Bun ORM là thư viện ORM dành cho Go theo hướng SQL-first, hỗ trợ lập trình viên xây dựng và thực thi truy vấn SQL, ánh xạ kết quả vào struct và làm việc với các quan hệ dữ liệu.

Điểm đáng chú ý của Bun là không cố gắng che giấu SQL hoàn toàn. Thay vào đó, thư viện cung cấp API để xây dựng truy vấn bằng Go trong khi vẫn giữ được cấu trúc gần với SQL.

Tuy nhiên, ORM chỉ là công cụ hỗ trợ tầng truy cập dữ liệu. Để xây dựng một ứng dụng Backend hiệu quả, lập trình viên vẫn cần hiểu cách database thực thi truy vấn, cách thiết kế schema, cách sử dụng index và transaction, cũng như cách xử lý lỗi.

Mục tiêu không chỉ là biết sử dụng các method của Bun, mà còn là hiểu được truy vấn nào đang được thực thi, dữ liệu được xử lý ra sao và vì sao cách triển khai đó phù hợp với bài toán.

Nguồn tham khảo

Bài viết khác

Redis & Caching là gì?

Redis & Caching là gì? Hướng dẫn sử dụng Redis trong Go 1. Caching là gì? Vì sao cần cache khi đã có database? Caching (bộ nhớ đệm) là kỹ thuật lưu tạm dữ liệu thường được truy cập để có thể lấy lại nhanh chóng khi cần, thay vì phải thực hiện lại toàn […]

Database Migration là gì?

1. Database Migration là gì? Database Migration là quá trình quản lý và áp dụng những thay đổi đối với cấu trúc hoặc dữ liệu trong cơ sở dữ liệu theo từng phiên bản của ứng dụng. Trong quá trình phát triển phần mềm, yêu cầu nghiệp vụ thường xuyên thay đổi. Một bảng có […]

Severity vs Priority(Độ nghiêm trọng vs Mức độ ưu tiên)

1.Severity và Priority là gì? Phân biệt ra sao? Trong bức tranh toàn cảnh của Vòng đời kiểm thử phần mềm (Software Testing Lifecycle – STLC), việc phát hiện ra lỗi (Bug/Defect) mới chỉ đóng góp 50% vào thành công của quy trình đảm bảo chất lượng. 50% còn lại phụ thuộc hoàn toàn vào […]

Bug Lifecycle & Bug Report

1.Bug Lifecycle & Bug Report là gì? Trong quản lý chất lượng phần mềm, Bug Lifecycle (Vòng đời của lỗi) và Bug Report (Báo cáo lỗi) là hai khái niệm cực kỳ quan trọng giúp kiểm thử viên (Tester) và lập trình viên (Developer) trao đổi, theo dõi và sửa lỗi một cách chuyên nghiệp. […]

Positive vs Negative Testing(Kiểm thử tích cực và Kiểm thử tiêu cực)

1.Positive vs Negative Testing là gì? Trong ngành công nghệ phần mềm, chất lượng sản phẩm không bao giờ là một khái niệm đơn chiều. Một ứng dụng được lập trình hoàn hảo trên lý thuyết vẫn có thể sụp đổ hoàn toàn ngay trong ngày đầu tiên phát hành nếu nó chỉ được chuẩn […]

Eloquent ORM

Eloquent ORM là gì? Hướng dẫn sử dụng Eloquent trong Laravel 1. Eloquent ORM là gì? Eloquent ORM là hệ thống Object-Relational Mapping (ORM) được tích hợp trong Laravel, một framework phát triển ứng dụng Web bằng PHP. Eloquent cho phép lập trình viên tương tác với cơ sở dữ liệu thông qua các class […]

Leave a Reply

Your email address will not be published. Required fields are marked *