Dùng sqlc quản lý Database trong Go: Hiệu suất thực tế và Type-safety tuyệt đối

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Chọn hướng đi cho Database trong Go: SQL thuần hay ORM?

Mỗi khi bắt đầu dự án Go mới, việc chọn thư viện làm việc với database luôn khiến anh em developer đau đầu. Thông thường, chúng ta sẽ đứng trước ba lựa chọn phổ biến:

  • Standard library (database/sql): Tốc độ nhanh nhất nhưng cực kỳ tốn công. Bạn phải map từng cột vào struct một cách thủ công. Chỉ cần schema thay đổi nhỏ, code sẽ dễ dàng bị crash.
  • ORM (GORM, Ent): Code rất nhanh và tiện lợi. Tuy nhiên, việc lạm dụng reflection khiến performance giảm đáng kể. Đôi khi, ORM tự sinh ra những câu query cồng kềnh với 5-6 tầng JOIN mà bạn khó lòng kiểm soát.
  • SQL Builders (Squirrel): Giải quyết được vấn đề viết query bằng code. Dù vậy, nó vẫn thiếu đi sự chặt chẽ về kiểu dữ liệu (type-safety) mà anh em backend luôn khao khát.

Sau nhiều dự án, mình rút ra kinh nghiệm: SQL vẫn là ngôn ngữ tốt nhất để truy vấn dữ liệu. Thay vì tìm cách né tránh SQL, chúng ta nên tìm cách tối ưu nó. Đó là lý do sqlc trở thành lựa chọn hàng đầu của mình.

sqlc: Khi SQL gặp sức mạnh của Compiler

sqlc không phải là một ORM thông thường. Thực chất, nó là một compiler mạnh mẽ. Bạn chỉ cần viết SQL thuần, sqlc sẽ đọc schema và tự động generate ra code Go tương ứng.

Điểm khác biệt nằm ở chỗ sqlc kiểm tra lỗi SQL ngay khi bạn gõ lệnh generate. Nếu bạn viết sai tên cột hoặc nhầm kiểu dữ liệu, compiler sẽ báo lỗi ngay lập tức. Bạn sẽ không còn phải lo lắng việc ứng dụng crash bất ngờ ở runtime do sai query.

Tại sao nên dùng sqlc?

  • Hiệu suất vượt trội: Code được generate là code Go thuần túy. Nó gọi trực tiếp vào driver database mà không qua bất kỳ tầng trung gian nào. Trong các bài benchmark, sqlc nhanh gấp 2-3 lần so với GORM.
  • Type-safety tuyệt đối: Mọi Input và Output đều được định nghĩa thành các struct rõ ràng. Điều này giúp tận dụng tối đa khả năng bắt lỗi của Go compiler.
  • Tự do tối ưu SQL: Bạn có thể sử dụng mọi tính năng đặc thù của Postgres hay MySQL. Bạn không còn bị giới hạn bởi những hàm trừu tượng của ORM.

Hạn chế cần cân nhắc:

  • Việc xử lý Dynamic SQL (ví dụ: query có filter thay đổi tùy ý) khá phức tạp.
  • Quy trình phát triển phụ thuộc vào bước generate code, yêu cầu team phải thống nhất workflow.

Kinh nghiệm thực tế từ dự án Fintech

Mình từng tham gia refactor một hệ thống Fintech với khoảng 50,000 dòng code. Ban đầu, team dùng GORM để phát triển nhanh. Khi lượng bản ghi đạt mức hàng triệu, các câu query bắt đầu chậm lại thấy rõ.

Khi chuyển sang sqlc, mình phát hiện ra hơn 15 bug tiềm ẩn. Đa số là lỗi sai kiểu dữ liệu giữa int4int8 mà GORM đã bỏ qua. Sau khi chuyển đổi, latency của các API quan trọng giảm từ 200ms xuống còn dưới 50ms.

Hướng dẫn triển khai sqlc trong 5 bước

Để bắt đầu, chúng ta sẽ quản lý một bảng authors đơn giản trên PostgreSQL.

Bước 1: Cài đặt công cụ

Cài đặt sqlc rất đơn giản qua Homebrew hoặc Go install:

# Dành cho macOS
brew install sqlc

# Hoặc cài trực tiếp bằng Go
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest

Bước 2: Cấu hình sqlc.yaml

Tạo file cấu hình để chỉ định nơi chứa SQL và nơi lưu code Go:

version: "2"
sql:
  - schema: "schema.sql"
    queries: "query.sql"
    engine: "postgresql"
    gen:
      go:
        package: "db"
        out: "db"

Bước 3: Viết Schema và Query

Định nghĩa cấu trúc bảng trong file schema.sql:

CREATE TABLE authors (
  id   BIGSERIAL PRIMARY KEY,
  name text      NOT NULL,
  bio  text
);

Viết các câu lệnh thao tác trong query.sql. Lưu ý phần -- name để đặt tên hàm:

-- name: GetAuthor :one
SELECT * FROM authors
WHERE id = $1 LIMIT 1;

-- name: CreateAuthor :one
INSERT INTO authors (name, bio) 
VALUES ($1, $2) 
RETURNING *;

Bước 4: Thực hiện Generate

Chạy lệnh sau trong terminal để tạo code:

sqlc generate

sqlc sẽ tự tạo folder db chứa toàn bộ logic truy vấn. Bạn tuyệt đối không nên sửa các file này bằng tay.

Bước 5: Tích hợp vào dự án Go

Bây giờ, việc gọi database trở nên rất ngắn gọn và an toàn:

func main() {
	ctx := context.Background()
	conn, _ := sql.Open("postgres", "user=postgres dbname=test sslmode=disable")

	queries := db.New(conn)

	// Tạo author mới với type-safety
	author, err := queries.CreateAuthor(ctx, db.CreateAuthorParams{
		Name: "Trần Văn B",
		Bio:  sql.NullString{String: "Backend Engineer", Valid: true},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("ID mới tạo:", author.ID)
}

Mẹo nhỏ để sử dụng hiệu quả

Để tối ưu quy trình làm việc, bạn nên áp dụng một vài kỹ thuật sau:

  1. Dùng Makefile: Gộp lệnh sqlc generate vào quy trình build để đảm bảo code luôn đồng bộ với SQL.
  2. Kết hợp công cụ Migration: sqlc không tự quản lý version database. Hãy dùng golang-migrate hoặc goose để quản lý schema chuyên nghiệp hơn.
  3. Tùy chỉnh kiểu dữ liệu: Nếu không thích sql.NullString, hãy dùng tính năng overrides trong config để chuyển sang con trỏ *string.

Nếu bạn cần một giải pháp cân bằng giữa tốc độ thực thi và sự an toàn, sqlc chính là mảnh ghép còn thiếu. Chúc anh em có những trải nghiệm viết SQL thật mượt mà trong Go!

Share: