Viết code CRUD Golang từ SQL

EminelEminel
10 min read

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 databasetruy 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ủ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": Chuyển Chuyển đổi kiểu uuid của SQL thành 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 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 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

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ả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

// 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

0
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.