Files
warehouse-management-BE/docs/sqlc/query.md
Tran Anh Tuan 6a4a96e0ca Base Project
2026-05-08 14:32:24 +07:00

10 KiB

Bộ quy tắc viết file query trong sqlc


1. Cấu trúc cơ bản

Mỗi file query là một file .sql thông thường, nhưng chứa sqlc annotation ở dạng SQL comment để sqlc phân tích metadata.

-- name: <TênMethod> <:command>
-- <comment mô tả (tuỳ chọn)>
SELECT * FROM users WHERE id = $1;

Cú pháp annotation:

-- name: <MethodName> <:command>
  • MethodName → Tên method sẽ được sinh trong Go code (phải là identifier hợp lệ, khuyến nghị dùng camelCase hoặc PascalCase).
  • :command → Loại thao tác, quyết định kiểu trả về.

2. Các loại :command

Command Kiểu trả về (Go) Dùng khi
:one (T, error) Trả về đúng 1 row. Nếu không tìm thấy → sql.ErrNoRows.
:many ([]T, error) Trả về danh sách rows. Có thể rỗng.
:exec (sql.Result, error) Thực thi không trả về row (INSERT/UPDATE/DELETE không cần data trả về).
:execrows (int64, error) Giống :exec nhưng trả về số rows affected.
:execresult (sql.Result, error) Giống :exec, trả về sql.Result đầy đủ (có .LastInsertId() + .RowsAffected()).
:copyfrom (int64, error) Dùng cho PostgreSQL COPY FROM, truyền slice struct.

3. Quy tắc viết annotation

3.1. Annotation phải nằm ngay trên câu query

-- ✅ ĐÚNG: annotation ngay trên query
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;

-- ❌ SAI: có dòng trống giữa annotation và query
-- name: GetUser :one

SELECT * FROM users WHERE id = $1;

3.2. Mỗi query phải kết thúc bằng dấu ;

-- ✅ ĐÚNG
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;

-- ❌ SAI: thiếu dấu ;
-- name: GetUser :one
SELECT * FROM users WHERE id = $1

3.3. Tên method không được trùng trong cùng một file (hoặc cùng package queries)

-- ❌ SAI: trùng tên
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;

-- name: GetUser :many
SELECT * FROM users WHERE role = $1;

3.4. Có thể viết nhiều query trong 1 file

-- name: GetUser :one
SELECT * FROM users WHERE id = $1;

-- name: ListUsers :many
SELECT * FROM users ORDER BY created_at DESC;

-- name: CreateUser :one
INSERT INTO users (id, username, email)
VALUES ($1, $2, $3)
RETURNING *;

-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;

4. Tham số (Parameters)

4.1. PostgreSQL — dùng positional params $1, $2, ...

-- name: CreateUser :one
INSERT INTO users (username, email, password_hash)
VALUES ($1, $2, $3)
RETURNING *;

4.2. Sử dụng sqlc.arg()khuyến nghị dùng vì rõ ràng hơn

-- name: CreateUser :one
INSERT INTO users (username, email, password_hash)
VALUES (sqlc.arg(username), sqlc.arg(email), sqlc.arg(password_hash))
RETURNING *;

Khi dùng sqlc.arg(), sqlc sinh tên param có ý nghĩa trong Go struct thay vì ID/Column2 không rõ ràng.

4.3. sqlc.narg() — nullable parameter

Dùng khi tham số có thể là NULL:

-- name: SearchUsers :many
SELECT * FROM users
WHERE (
    sqlc.narg(username) IS NULL OR username = sqlc.narg(username)
)
AND (
    sqlc.narg(email) IS NULL OR email = sqlc.narg(email)
);

sqlc sẽ sinh kiểu NullString hoặc con trỏ *string (tuỳ config) cho các param này.

4.4. Truyền struct/array — sqlc.arg() với nhiều field

-- name: UpdateUser :one
UPDATE users
SET
    username = COALESCE(sqlc.arg(username), username),
    email = COALESCE(sqlc.arg(email), email)
WHERE id = sqlc.arg(id)
RETURNING *;

5. RETURNING * — Khuyến nghị dùng với :one / :many

Khi INSERT, UPDATE, DELETE mà bạn muốn nhận lại dữ liệu, hãy dùng RETURNING *:

-- name: CreateUser :one
INSERT INTO users (username, email)
VALUES ($1, $2)
RETURNING *;

-- name: UpdateUser :one
UPDATE users SET email = $2 WHERE id = $1
RETURNING *;

-- name: DeleteUser :one
DELETE FROM users WHERE id = $1
RETURNING *;

Nếu không có RETURNING *, phải dùng :exec hoặc :execrows.


6. Quy tắc cho SELECT *

sqlc sẽ phân tích schema và thay * bằng danh sách cột cụ thể trong code sinh ra. Tuy nhiên:

-- ✅ Khuyến nghị: chỉ định cột rõ ràng
-- name: GetUser :one
SELECT id, username, email, created_at FROM users WHERE id = $1;

-- ✅ Cũng hợp lệ: dùng *
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;

Khi JOIN nhiều bảng, KHÔNG nên dùng SELECT * vì có thể trùng tên cột. Nên alias rõ ràng.


7. JOIN và Alias

-- name: GetUserWithRole :one
SELECT
    u.id,
    u.username,
    u.email,
    r.name AS role_name
FROM users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN roles r ON ur.role_id = r.id
WHERE u.id = $1;

sqlc sẽ sinh struct với các field ID, Username, Email, RoleName.


8. Sử dụng sqlc.embed()

Dùng khi muốn nhúng toàn bộ một struct vào kết quả (từ sqlc v1.18+):

-- name: GetUserWithRole :one
SELECT
    sqlc.embed(u),
    r.name AS role_name
FROM users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN roles r ON ur.role_id = r.id
WHERE u.id = $1;

→ Go struct sinh ra sẽ có dạng:

type GetUserWithRoleRow struct {
    User     // embedded struct từ bảng users
    RoleName string
}

9. Sử dụng sqlc.slice() — IN clause

Dùng cho WHERE id IN ($1, $2, ...) với số lượng phần tử động:

-- name: GetUsersByIDs :many
SELECT * FROM users
WHERE id = ANY(sqlc.slice(ids));

hoặc với PostgreSQL:

-- name: GetUsersByIDs :many
SELECT * FROM users
WHERE id = ANY($1::uuid[]);

Nhưng sqlc.slice() được khuyến nghị hơn vì sqlc sẽ tự xử lý kiểu.


10. Sub-query và CTE

sqlc hỗ trợ đầy đủ:

-- name: GetUserStats :one
WITH user_orders AS (
    SELECT user_id, COUNT(*) AS order_count
    FROM orders
    GROUP BY user_id
)
SELECT u.*, COALESCE(uo.order_count, 0) AS order_count
FROM users u
LEFT JOIN user_orders uo ON u.id = uo.user_id
WHERE u.id = $1;

11. CASE expression

-- name: ListUsersWithStatus :many
SELECT
    id,
    username,
    CASE
        WHEN deleted_at IS NOT NULL THEN 'deleted'
        WHEN last_login_at > NOW() - INTERVAL '30 days' THEN 'active'
        ELSE 'inactive'
    END AS status
FROM users;

12. Transaction — không viết trong file query

sqlc không quản lý transaction trong file .sql. Transaction được xử lý ở tầng ứng dụng Go:

// Trong Go code (không phải file .sql)
tx, _ := db.BeginTx(ctx, nil)
q := New(tx) // tạo Queries instance với tx
q.CreateUser(ctx, ...)
q.CreateUserRole(ctx, ...)
tx.Commit()

13. Comment thường vs sqlc annotation

-- Đây là comment thường, sqlc bỏ qua
-- name: GetUser :one   ← Đây là sqlc annotation
SELECT * FROM users WHERE id = $1;

/*
  Multi-line comment cũng được
  sqlc bỏ qua
*/

14. Quy tắc đặt tên file

Quy ước Ví dụ
1 file = 1 bảng chính users.sql, orders.sql, products.sql
Đặt tên theo feature/domain auth.sql, inventory.sql
File nằm trong thư mục được chỉ định ở queries trong sqlc.yaml ./db/queries/

15. Toàn bộ mẫu tham khảo

-- name: GetUser :one
SELECT * FROM users WHERE id = sqlc.arg(id);

-- name: GetUserByEmail :one
SELECT * FROM users WHERE email = sqlc.arg(email);

-- name: ListUsers :many
SELECT * FROM users
ORDER BY created_at DESC
LIMIT sqlc.arg(limit) OFFSET sqlc.arg(offset);

-- name: SearchUsers :many
SELECT * FROM users
WHERE (
    sqlc.narg(username) IS NULL OR username ILIKE '%' || sqlc.narg(username) || '%'
)
AND (
    sqlc.narg(email) IS NULL OR email ILIKE '%' || sqlc.narg(email) || '%'
);

-- name: CreateUser :one
INSERT INTO users (id, username, email, password_hash)
VALUES (
    sqlc.arg(id),
    sqlc.arg(username),
    sqlc.arg(email),
    sqlc.arg(password_hash)
)
RETURNING *;

-- name: UpdateUser :one
UPDATE users
SET
    username = COALESCE(sqlc.arg(username), username),
    email = COALESCE(sqlc.arg(email), email)
WHERE id = sqlc.arg(id)
RETURNING *;

-- name: DeleteUser :execrows
DELETE FROM users WHERE id = sqlc.arg(id);

-- name: GetUsersByIDs :many
SELECT * FROM users WHERE id = ANY(sqlc.slice(ids));

-- name: CountUsers :one
SELECT COUNT(*) AS count FROM users;

Tóm tắt nhanh

Quy tắc Mô tả
Annotation format -- name: MethodName :command
Phải có ; kết thúc Mỗi query kết thúc bằng dấu chấm phẩy
Annotation ngay trên query Không có dòng trống ở giữa
Tên method không trùng Trong cùng package queries
Dùng sqlc.arg() Khuyến nghị thay vì $1 để sinh tên param có ý nghĩa
Dùng sqlc.narg() Cho nullable parameter
Dùng sqlc.slice() Cho IN (...) dynamic
Dùng sqlc.embed() Để nhúng struct khi JOIN
RETURNING * Khi cần dữ liệu trả về với INSERT/UPDATE/DELETE
Transaction Không viết trong .sql, xử lý ở Go code