Viết code CRUD Golang từ SQL


Trong quá trình phát triển ứng dụng, việc viết code CRUD (Create, Read, Update, Delete) từ database thường chiếm nhiều thời gian và dễ gây lỗi nếu thực hiện thủ công. Việc tự động generate CRUD code trong Golang từ SQL giúp tăng tốc độ phát triển, đảm bảo tính nhất quán và giảm thiểu sai sót.
Bài viết này sẽ hướng dẫn bạn cách sinh mã Golang tự động từ các bảng SQL, giúp bạn tiết kiệm thời gian và tập trung vào logic nghiệp vụ thay vì xử lý các thao tác cơ bản với database.
So sánh các thư viện Golang để generate CRUD code từ SQL
Khi tạo CRUD (Create, Read, Update, Delete) trong Golang, có một số thư viện và phương pháp phổ biến được sử dụng, bao gồm database/sql
, GORM, sqlx, và SQLC. Dưới đây là so sánh giữa chúng:
Database/sql
Tổng quan: Đây là thư viện SQL chuẩn của Go, cung cấp giao diện cơ bản để tương tác với các cơ sở dữ liệu SQL.
Ưu điểm:
Hiệu suất cao, vì nó là thư viện chuẩn(đã được tối ưu) và không tạo thêm gánh nặng xử lý hay bộ nhớ.
Toàn quyền kiểm soát truy vấn SQL.
Hỗ trợ tất cả các database có driver tương thích với
database/sql
.
Nhược điểm:
Phải tự viết nhiều code boilerplate (kết nối, chuẩn bị câu lệnh, scan kết quả).
Không hỗ trợ ORM (Object-Relational Mapping) tự động
➡ Phù hợp khi: Cần hiệu suất tối đa, chấp nhận viết nhiều code thủ công.
Ví dụ:
import (
"database/sql"
_ "github.com/denisenkom/go-mssqldb"
)
func insertData(db *sql.DB, username, email string) error {
query := "INSERT INTO users (username, email) VALUES (?,?)"
_, err := db.ExecContext(context.Background(), query, username, email)
return err
}
GORM
Tổng quan: GORM là một thư viện ORM (Object-Relational Mapping) mạnh mẽ cho Go, giúp ánh xạ các đối tượng Go sang bảng cơ sở dữ liệu.
Ưu điểm:
Hỗ trợ ORM đầy đủ (mapping struct với table).
Cung cấp nhiều tính năng như auto migrations, hooks, eager loading.
Hỗ trợ nhiều loại database như Postgres, MySQL, SQLite, MSSQL.
Nhược điểm:
Overhead cao hơn
database/sql
vì sử dụng reflection.Debug truy vấn SQL khó hơn vì bị wrap trong ORM layer.
Hiệu suất không tối ưu cho các truy vấn phức tạp.
➡ Phù hợp khi: Muốn code nhanh, dễ bảo trì, không quá quan trọng hiệu suất.
Ví dụ:
package main
import (
"gorm.io/gorm"
"gorm.io/driver/sqlite"
)
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// Migrate the schema
db.AutoMigrate(&Product{})
// Create
db.Create(&Product{Code: "D42", Price: 100})
// Read
var product Product
db.First(&product, 1) // find product with integer primary key
db.First(&product, "code = ?", "D42") // find product with code D42
// Update - update product's price to 200
db.Model(&product).Update("Price", 200)
// Update - update multiple fields
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
// Delete - delete product
db.Delete(&product, 1)
}
SQLX
Tổng quan: sqlx là một thư viện mở rộng của database/sql
, cung cấp các tính năng như hỗ trợ struct và slice tự động.
Ưu điểm:
Giữ hiệu suất như
database/sql
nhưng có API tiện dụng hơn.Hỗ trợ scan dữ liệu trực tiếp vào struct mà không cần nhiều code boilerplate.
Hỗ trợ bind named query (
:param
) giúp viết truy vấn SQL dễ hơn.
Nhược điểm:
Không có ORM, vẫn phải viết SQL bằng tay.
Vẫn cần viết nhiều code, phát hiện lỗi tại runtime.
➡ Phù hợp khi: Muốn hiệu suất cao nhưng vẫn tiện lợi, không cần ORM.
Ví dụ:
import (
"github.com/jmoiron/sqlx"
)
type User struct {
ID int `db:"id"`
Username string `db:"username"`
Email string `db:"email"`
}
func queryData(db *sqlx.DB) ([]User, error) {
query := "SELECT * FROM users"
var users []User
err := db.Select(&users, query)
return users, err
}
SQLC
Tổng quan: SQLC là một công cụ giúp sinh ra mã Go từ các truy vấn SQL, hỗ trợ các hoạt động CRUD một cách hiệu quả.
Ưu điểm:
Tự động sinh code Golang từ SQL, không cần viết code query.
Hiệu suất như
database/sql
, không có overhead như ORM.Hỗ trợ kiểm tra lỗi truy vấn SQL ngay từ lúc compile.
Nhược điểm:
Cần cấu hình và chạy lệnh để sinh mã.
Không có ORM, vẫn phải viết truy vấn SQL nhưng theo cách tự động hóa hơn.
Hỗ trợ hạn chế cho các DB ngoài Postgres, MySQL, SQLite
➡ Phù hợp khi: Dùng Postgres, MySQL, SQLite thích viết SQL trực tiếp nhưng muốn tự động hóa code Golang.
Ví dụ:
package db
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
const createAuthor = `-- name: CreateAuthor :exec
INSERT INTO authors (bio) VALUES ($1)
`
func (q *Queries) CreateAuthor(ctx context.Context, bio string) error {
_, err := q.db.ExecContext(ctx, createAuthor, bio)
return err
}
Kết luận
Trong series này tôi sẽ sử dụng sqlc! Nó chạy rất nhanh, giống như database/sql
, và cực kỳ dễ sử dụng. Điểm hay nhất là bạn chỉ cần viết các truy vấn SQL, sqlc
sẽ tự động tạo ra code CRUD Golang cho bạn.
Ví dụ, bạn chỉ cần cung cấp schema của database và truy vấn SQL cho sqlc
. Mỗi truy vấn sẽ có một dòng comment phía trên để hướng dẫn sqlc
tạo đúng hàm tương ứng.
Sau đó, sqlc
sẽ sinh ra code Golang theo chuẩn, sử dụng thư viện database/sql
. Vì sqlc
phân tích truy vấn SQL trước khi tạo code, nên mọi lỗi sẽ được phát hiện ngay lập tức. Quá tiện lợi đúng không?
Tuy nhiên, sqlc
hiện tại chỉ hỗ trợ tốt nhất cho Postgres, MySQL, SQLite. Vì vậy, nếu bạn dùng Postgres, MySQL, SQLite sqlc
là lựa chọn lý tưởng. Nếu không, bạn có thể cân nhắc dùng sqlx
.
Cài đặt Sqlc
Để cài đặt sqlc chúng ta truy cập vào đây
Có nhiều cách cài đặt tuy nhiên trong series này tôi sử dụng lệnh sau:
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
Sau khi cài đặt thành công chúng ta kiểm tra lại version
của sqlc
.
sqlc version
Viết a setting file
Bây giờ chúng ta sẽ đi đến thư mục dự án simple_bank
mà đang làm việc, khởi tạo một module Go mới tên là github.com/eminel9311/simplebank, thực ra tên của module các bạn muốn đặt gì cũng được, nó là một chuỗi string thôi.
go mod init github.com/eminel9311/simplebank
trong project của chúng ta sẽ có một file tên là go.mod
được sinh ra, có nội dung như sau
module github.com/eminel9311/simplebank
go 1.23.7
Tiếp tục tại thư mục simple_bank
hiện tại, tạo một file tên là sqlc.yaml
hoặc sử dụng lệnh sqlc init
để tạo file này. File này có nội dung như sau
version: "2"
sql:
- schema: "db/migration"
queries: "db/query"
engine: "postgresql"
gen:
go:
package: "db"
out: "db/sqlc"
sql_package: "pgx/v5"
emit_json_tags: true
emit_interface: true
emit_empty_slices: true
overrides:
- db_type: "timestamp"
go_type: "time.Time"
- db_type: "uuid"
go_type: "github.com/google/uuid.UUID"
version: "2"
: Xác định phiên bản cấu hình củasqlc
.schema: "db/migration"
: Thư mục chứa các file.sql
định nghĩa schema database, giúp sqlc đọc cấu trúc database và tạo ra entity (struct Go) tương ứng.queries: "db/query"
: Thư mục chứa các file.sql
chứa các truy vấn (SELECT
,INSERT
,UPDATE
,DELETE
).engine: "postgresql"
: Chỉ định database engine là PostgreSQL.package: "db"
: Tên package Go được generate.out: "db/sqlc"
: Thư mục chứa code Go được generate.sql_package: "pgx/v5"
: Thư viện Go sẽ được sử dụng để tương tác với PostgreSQL.emit_json_tags: true
: Thêm JSON tags vào struct Go.emit_interface: true
: Tạo interface cho các query, giúp dễ mock khi test.emit_empty_slices: true
: Trả về slice rỗng ([]
) thay vìnil
nếu không có dữ liệu.db_type: "timestamp" -> go_type: "time.Time"
: Chuyển đổi kiểutimestamp
của SQL thànhtime.Time
trong Go.db_type: "uuid" -> go_type: "
github.com/google/uuid.UUID
"
: Chuyển Chuyển đổi kiểuuuid
của SQL thànhgithub.com/google/uuid.UUID
trong Go.
Tạo các file queries
Tại sao cần tạo file này?
Các File này chứa các câu SQL query mà bạn muốn sử dụng trong ứng dụng
sqlc
sẽ đọc file này và tự động generate code Go tương ứngThay vì phải viết code Go thủ công để tương tác với database,
sqlc
sẽ làm điều này cho bạn
Tôi sẽ viết mẫu một file db/query/account.sql
các file khác các bạn có thể xem thêm ở git nhé.
-- name: CreateAccount :one
INSERT INTO accounts (
owner,
balance,
currency
) VALUES (
$1, $2, $3
) RETURNING *;
-- name: GetAccount :one
SELECT * FROM accounts
WHERE id = $1 LIMIT 1;
-- name: ListAccounts :many
SELECT * FROM accounts
ORDER BY id
LIMIT $1
OFFSET $2;
-- name: UpdateAccount :one
UPDATE accounts
SET balance = $2
WHERE id = $1
RETURNING *;
-- name: DeleteAccount :exec
DELETE FROM accounts
WHERE id = $1;
-- name
sau key này sẽ là tên của hàm sẽ được Go sinh raCreateAccount
sẽ được chuyển thành function GoCreateAccount
:one
nghĩa là function sẽ trả về một record:many
nghĩa là trả về nhiều records:exec
nghĩa là không cần trả về dữ liệu$1, $2
là tham số đầu vào trong Go
Run sqlc generate command
Okay, bây giờ hãy mở teminal và run
sqlc generate
Các bạn sẽ thấy có các file sau đã được sinh ra
Hiện tại khi truy cập các file db.go
models.go
sẽ gặp các lỗi kiểu như sau
could not import github.com/jackc/pgx/v5/pgtype (no required module provides package "github.com/jackc/pgx/v5/pgtype")
Để khắc phục các bạn có thể chạy lệnh sau để thêm các dependencies vào go.mod
go mod tidy
Lệnh go mod tidy
được sử dụng trong Go để dọn dẹp và cập nhật các phụ thuộc (dependencies) trong file go.mod
của dự án.
Các file được sinh ra bởi sqlc
Đầu tiên chúng ta sẽ cùng nhau xem qua file db/sqlc/account.sql.go
package db
import (
"context"
)
const createAccount = `-- name: CreateAccount :one
INSERT INTO accounts (
owner,
balance,
currency
) VALUES (
$1, $2, $3
) RETURNING id, owner, balance, currency, created_at
`
type CreateAccountParams struct {
Owner string `json:"owner"`
Balance int64 `json:"balance"`
Currency string `json:"currency"`
}
func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) {
row := q.db.QueryRow(ctx, createAccount, arg.Owner, arg.Balance, arg.Currency)
var i Account
err := row.Scan(
&i.ID,
&i.Owner,
&i.Balance,
&i.Currency,
&i.CreatedAt,
)
return i, err
}
// còn tiếp
Đây là một hàm trong package
db
dùng để tạo tài khoản mới trong bảngaccounts
.Sử dụng SQL
INSERT ... RETURNING
để thêm dữ liệu và lấy thông tin ngay sau khi chèn.Dữ liệu đầu vào được ánh xạ bằng struct
CreateAccountParams
.Kết quả được ánh xạ vào struct
Account
và trả về cùng lỗi (nếu có).
Tiếp theo cùng nhau xem qua file db/sqlc/models.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
package db
import (
"github.com/jackc/pgx/v5/pgtype"
)
type Account struct {
ID int64 `json:"id"`
Owner string `json:"owner"`
Balance int64 `json:"balance"`
Currency string `json:"currency"`
CreatedAt pgtype.Timestamp `json:"created_at"`
}
type Entry struct {
ID int64 `json:"id"`
AccountID int64 `json:"account_id"`
// can be negative or positive
Amount int64 `json:"amount"`
CreatedAt pgtype.Timestamp `json:"created_at"`
}
type Transfer struct {
ID int64 `json:"id"`
FromAccountID int64 `json:"from_account_id"`
ToAccountID int64 `json:"to_account_id"`
// must be positive
Amount int64 `json:"amount"`
CreatedAt pgtype.Timestamp `json:"created_at"`
}
File này định nghĩa các struct giúp ánh xạ dữ liệu từ database vào Go để dễ dàng thao tác.
Thực ra còn một số file như db.go
, querier.go
nữa nhưng các file này là lớp trung gian để thực thi truy vấn SQL trong PostgreSQL, được tạo tự động bởi sqlc. Ở các bài tiếp theo nếu có dịp tôi sẽ nói thêm. Bài viết hôm nay khá dài rồi nên xin phép được kết thúc tại đây, hi vọng các bạn thích nội dung này. Cám ơn bạn đọc.
Bài viết gốc: https://dev.to/techschoolguru/generate-crud-golang-code-from-sql-and-compare-db-sql-gorm-sqlx-sqlc-560j
Subscribe to my newsletter
Read articles from Eminel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Eminel
Eminel
Hello, my name is Eminel a software engineer specializing in scalable software architecture, microservices, and AI-powered platforms. Passionate about mentoring, performance optimization, and building solutions that drive business success.