Bài 1: Vấn đề của việc truy vấn trực tiếp & Tư duy thiết kế Database Wrapper
Trong các ứng dụng Backend Golang, việc thao tác với cơ sở dữ liệu (Database) là một phần cốt lõi không thể thiếu. Tuy nhiên, khi dự án phát triển từ vài chục bảng dữ liệu lên hàng trăm bảng với hàng ngàn dòng code nghiệp vụ, cách tiếp cận viết câu lệnh truy vấn trực tiếp thường bộc lộ những điểm yếu chí mạng.
Trong bài học mở đầu này, chúng ta sẽ mổ xẻ những hệ lụy của việc truy vấn lộn xộn và làm quen với Tư duy thiết kế Database Wrapper (Repository Pattern & Abstraction) để xây dựng một tầng dữ liệu sạch sẽ, bền vững.
1. Vấn đề của việc truy vấn trực tiếp (Tight Coupling)
Các lập trình viên mới hoặc trong các dự án nhỏ thường có thói quen gọi thẳng các thư viện như sqlx, database/sql nguyên thủy, hay thậm chí nhúng trực tiếp các câu lệnh ORM (như GORM) vào bên trong tầng xử lý nghiệp vụ (Service hoặc Handler).
Go
// TỆ: Business logic dính chặt (tightly coupled) với thư viện ORM và câu lệnh SQL cụ thể
func (s *UserService) UpdateUserEmail(userID int64, newEmail string) error {
// Gọi trực tiếp GORM trong service
result := s.db.Model(&User{}).Where("id = ?", userID).Update("email", newEmail)
if result.Error {
return result.Error
}
return nil
}
Những hệ quả nghiêm trọng:
-
Phụ thuộc chặt chẽ (Tight Coupling): Tầng nghiệp vụ của bạn bị trói buộc hoàn toàn vào một thư viện cụ thể (ví dụ GORM). Nếu công ty quyết định chuyển sang sử dụng
sqlxthuần túy hoặc đổi từ MySQL sang PostgreSQL với các cú pháp đặc thù, bạn sẽ phải sửa lại hàng loạt code ở tầng business. -
Ác mộng khi viết Unit Test: Việc viết Unit Test cho tầng Service trở nên cực kỳ khó khăn vì service bị dính kết nối thật với database hoặc phải dựng các mock phức tạp của thư viện ngoài.
-
Trùng lặp mã nguồn (Code Duplication): Các câu lệnh truy vấn, phân trang (pagination), hay lọc dữ liệu bị lặp đi lặp lại ở nhiều controller khác nhau.
2. Tư duy thiết kế Database Wrapper (Repository Pattern)
Để giải quyết triệt để vấn đề trên, chúng ta áp dụng Repository Pattern kết hợp với Abstraction Layer (Lớp trừu tượng).
Nguyên tắc cốt lõi: Tầng nghiệp vụ (Service) không cần biết dữ liệu được lưu bằng GORM, sqlx hay lưu trên file JSON; nó chỉ cần biết "Tôi muốn lấy ra User ID này".
Plaintext
[ Business Logic / Services ]
│
▼ (Chỉ giao tiếp qua Interface)
[ Repository Interface ] <-- (Định nghĩa hợp đồng dữ liệu)
│
▼ (Hiện thực hóa - Implementation)
[ Database Wrapper: GORM / SQLX / PostgreSQL ]
Lợi ích tuyệt đối của Database Wrapper:
-
Cô lập công nghệ: Toàn bộ chi tiết câu lệnh SQL, tên cột, tên bảng được gói gọn bên trong tầng Repository. Tầng Service hoàn toàn "trong sạch" (pure business logic).
-
Dễ dàng Mocking & Testing: Bạn có thể dễ dàng tạo ra các mock object cho Repository để viết Unit Test cho Service trong vài giây mà không cần database thật.
-
Linh hoạt thay thế: Thay đổi cơ chế lưu trữ bên dưới mà không làm ảnh hưởng đến logic nghiệp vụ đã viết.
3. Thiết kế Repository Interface chuẩn trong Go
Một Database Wrapper chuẩn mực thường định nghĩa các interface rõ ràng cho từng thực thể (Entity) trong hệ thống:
Go
package repository
import (
"context"
"your-project/internal/domain" // Chứa các struct entity thuần túy
)
// UserRepository định nghĩa các hành vi thao tác dữ liệu với bảng User
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*domain.User, error)
Create(ctx context.Context, user *domain.User) error
UpdateEmail(ctx context.Context, id int64, email string) error
}
Và tầng hiện thực hóa (Store/Implementation) sẽ nằm tách biệt:
Go
package postgres
import (
"context"
"database/sql"
"your-project/internal/domain"
)
// sqlUserRepo hiện thực hóa UserRepository sử dụng database/sql hoặc sqlx
type sqlUserRepo struct {
db *sql.DB
}
func NewSQLUserRepo(db *sql.DB) *sqlUserRepo {
return &sqlUserRepo{db: db}
}
func (r *sqlUserRepo) FindByID(ctx context.Context, id int64) (*domain.User, error) {
// Toàn bộ câu lệnh SQL chi tiết nằm gọn ở đây
query := `SELECT id, name, email FROM users WHERE id = $1`
var user domain.User
err := r.db.QueryRowContext(ctx, query, id).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}
func (r *sqlUserRepo) Create(ctx context.Context, user *domain.User) error {
// Logic insert...
return nil
}
func (r *sqlUserRepo) UpdateEmail(ctx context.Context, id int64, email string) error {
// Logic update...
return nil
}
Tổng kết
Bằng cách áp dụng Database Wrapper và Repository Pattern, chúng ta đã tách biệt hoàn toàn tầng logic nghiệp vụ khỏi các chi tiết kỹ thuật của cơ sở dữ liệu, giúp mã nguồn dễ bảo trì, dễ kiểm thử và có tính mở rộng cao.
Tuy nhiên, trước khi viết các hàm repository, việc đầu tiên chúng ta cần làm là thiết lập kết nối chuẩn chỉnh tới cơ sở dữ liệu và quản lý Connection Pool sao cho không bị rò rỉ tài nguyên khi hệ thống chịu tải cao.
Đó chính là nội dung của Bài 2 tiếp theo.
All rights reserved