# 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](https://pkg.go.dev/database/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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1742359286308/7e1ae98b-c842-47e2-a7e6-82d1cc4a5f97.png align="center")

**Ư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ụ:

```go
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](https://gorm.io/docs/) 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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1742359352054/5e39bbd4-e9a6-48fe-95e4-0f49244d9486.png align="center")

**Ư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ụ:

```go
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](https://github.com/jmoiron/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ụ:

```go
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](https://docs.sqlc.dev/en/latest/) 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ụ:

```go
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](https://docs.sqlc.dev/en/latest/overview/install.html)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1742373930926/1fe7c7a3-37ec-4ae4-9920-243de63ef3b3.png align="center")

Có nhiều cách cài đặt tuy nhiên trong series này tôi sử dụng lệnh sau:

```bash
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`.

```bash
sqlc version
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1742374130534/b3d8a5f2-e87e-490c-a563-7b92d5c5bf83.png align="left")

### 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](http://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.

```bash
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

```go
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

```yaml
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ủa `sqlc`.
    
* `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ểu `timestamp` của SQL thành `time.Time` trong Go.
    
* `db_type: "uuid" -> go_type: "`[`github.com/google/uuid.UUID`](http://github.com/google/uuid.UUID)`"`: Chuyển Chuyển đổi kiểu `uuid` của SQL thành [`github.com/google/uuid.UUID`](http://github.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 ứng
    
* Thay 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](https://github.com/eminel9311/simplebank) nhé.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1742381869424/0a9198ae-7263-4f8e-8508-27968ecb4afb.png align="left")

```sql
-- 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 ra
    
* `CreateAccount` sẽ được chuyển thành function Go `CreateAccount`
    
* `: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

```bash
sqlc generate
```

Các bạn sẽ thấy có các file sau đã được sinh ra

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1742382293309/82835846-faaa-47e0-8d76-5eb9525e750e.png align="left")

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

```bash
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`

```bash
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`

```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ảng `accounts`.
    
* 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`

```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](https://dev.to/techschoolguru/generate-crud-golang-code-from-sql-and-compare-db-sql-gorm-sqlx-sqlc-560j)
