401 lines
10 KiB
Markdown
401 lines
10 KiB
Markdown
# 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: <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
|
|
|
|
```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 |
|