# 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. ```sql -- name: <:command> -- SELECT * FROM users WHERE id = $1; ``` **Cú pháp annotation:** ``` -- name: <: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 ```sql -- ✅ ĐÚ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 `;` ```sql -- ✅ ĐÚ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) ```sql -- ❌ 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 ```sql -- 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, ...` ```sql -- 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 ```sql -- 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`: ```sql -- 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 ```sql -- 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 *`: ```sql -- 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: ```sql -- ✅ 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 ```sql -- 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+): ```sql -- 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: ```go 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: ```sql -- name: GetUsersByIDs :many SELECT * FROM users WHERE id = ANY(sqlc.slice(ids)); ``` hoặc với PostgreSQL: ```sql -- 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 đủ: ```sql -- 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 ```sql -- 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: ```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 ```sql -- Đâ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 ```sql -- 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 |