🎯 什麼情境該想到我
當你「想用設定或註解描述物件與資料表的對應,讓框架自動幫你產生存取程式碼」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:把「物件欄位 ↔ 資料表欄位」的對應關係,用中繼資料(外部設定檔或程式內註解)描述,而不是把對應邏輯寫死在每個類別裡。
做法要點:
- 用設定檔(如 XML/YAML)或程式碼註解描述每個類別對應哪張表、每個屬性對應哪個欄位。
- 框架在執行期讀這些中繼資料,可用兩種方式運作:
- 反射式:執行時依中繼資料動態讀寫欄位。
- 產生碼式:事先依中繼資料產生存取原始碼再編譯。
- 這是 ORM 框架(如 Hibernate)的核心手法,讓對應集中、可維護。
★★ 兩條路:執行期反射 vs 編譯期產生
★ ① 執行期反射(Rails ActiveRecord、GORM、Hibernate、Prisma runtime)
→ 讀 struct tag/schema,在執行期組 SQL
→ ★ 彈性最大、樣板最少
→ ★★ 代價:型別安全在執行期才驗、效能有反射成本、SQL 不可見
★ ② ★★ 編譯期程式碼產生(sqlc、ent、sqlboiler、Prisma Client)
→ 讀 schema/SQL,產生具體的 Go 程式碼
→ ★ 型別安全在編譯期、零反射成本、產生的碼可讀可 debug
→ ★ 代價:多一個 generate 步驟,產生的碼要進版控
★★ Go 生態壓倒性地選 ②,理由和整個語言的取向一致:
把「執行期才會爆的錯」搬到編譯期(見 表格資料閘道)。
★ Go 的三種寫法對照
// ① ★ struct tag(反射式元資料)—— GORM/sqlx
type Order struct {
ID string `db:"id" gorm:"primaryKey"`
Status string `db:"status"`
Total int64 `db:"total_cents"`
}
// ★★ 陷阱:tag 是字串,打錯不會編譯錯誤,只會在執行期 scan 失敗或靜默漏欄位
// ② ★★ sqlc:SQL 是真相,Go 由它產生
-- name: GetOrder :one
SELECT id, status, total_cents FROM orders WHERE id = $1;
// → 產生 func (q *Queries) GetOrder(ctx, id string) (Order, error)
// ★ 改 SQL 忘了 regenerate → ★ 編譯錯誤(欄位對不上)
// ③ ★ ent:schema 是真相,SQL 與 Go 都由它產生
// → ★ 最完整(含關聯、圖查詢),★ 但學習成本最高、逃生門較窄★★ 這個模式最大的隱憂:元資料成為第二個真相來源
★ 三個「真相」可能不一致:
① 資料庫的實際 schema
② 元資料(struct tag/schema 檔)
③ migration 檔
→ ★★ 任兩者不一致 = 執行期才爆,而且錯誤訊息通常很難懂
("column orders.total does not exist" 出現在一個你沒改過的查詢裡)
★ 三個具體對策:
① ★★ 讓 schema 只有一個來源
✅ migration 是真相 → sqlc 讀 migration 產生型別(★ 推薦)
✅ 或 schema 檔是真相 → 由它產生 migration(ent/Prisma 的做法)
❌ 兩邊各自維護 —— 一定會不一致
② ★★ CI 要跑「schema 與產生的碼是否同步」的檢查
sqlc generate && git diff --exit-code ← ★ 忘了 regenerate 就 CI 紅燈
→ ★ 這一行是這個模式最重要的防護
③ ★ 對真實資料庫跑一次 smoke 查詢
→ ★ 型別對得上不代表資料庫真的有那個欄位(見 [[服務樁]])
★★ 元資料驅動的共同陷阱:它把 schema 變成公開契約
// ★ struct tag 決定了 JSON 與 DB 兩套對映
type Order struct {
TotalCents int64 `json:"total_cents" db:"total_cents"`
}
// ★★ 改欄位名 → 同時破壞 API 契約與資料庫對映
// → ★ 而編譯器對兩者都不保護★★ 對策:領域模型、DB row、API DTO 是三個型別,
tag 只出現在後兩者上(見 資料傳輸物件、對應器)。
★ 這是多寫一點樣板換取「三者可以獨立演進」。
★ 什麼時候不需要元資料對應
- ★ 查詢很少、都是手寫 SQL → 直接
rows.Scan(&a, &b)就好。
⚠️ 但Scan的參數順序必須和SELECT的欄位順序一致——這是手寫的主要風險。 - ★ 一次性腳本/遷移工具。
- ★ 極端效能敏感的路徑 → 手寫 scan 沒有反射成本(★ 但差異通常在 ns 級,先 profile)。
★ 兄弟辨析
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
反射式較慢且較難除錯;中繼資料錯了問題不易追。
-
對應很簡單、只有少數類別時,直接手寫對應反而清楚。
-
中繼資料會變成另一套要學的「隱形語言」,需良好文件與工具支援。
-
★★ CI 一定要跑
generate && git diff --exit-code。忘了 regenerate 是這個模式的頭號 bug 來源。 -
★★ schema 只能有一個真相來源(migration 或 schema 檔,不要兩邊各自維護)。
-
★★ struct tag 是字串,打錯不會編譯錯誤——只會執行期 scan 失敗或靜默漏欄位。
-
★★ 同一個 struct 同時背
json與dbtag,會讓 API 契約與 DB schema 綁死。 領域/row/DTO 要是三個型別。 -
★ 型別對得上不代表資料庫真的有那個欄位。 CI 要對真實資料庫跑一次 smoke 查詢。
-
★ 手寫
rows.Scan的風險是「參數順序必須和 SELECT 一致」,加一個欄位就整排錯位。 -
反射的效能成本通常在 ns 級,不要拿它當選擇理由——型別安全才是。
-
產生的程式碼要進版控,否則 code review 看不到查詢實際變成什麼。
🔗 相關工具
- 查詢物件 Query Object(在中繼資料之上,用物件描述查詢條件)
- 儲存庫 Repository(常以中繼資料對應為底層,對外提供集合式介面)
- 企業應用架構模式
- 資料對應器 —— 手寫對應的對照
- 查詢物件 —— 建立在元資料之上
- 儲存庫 —— 集合式查詢介面
- 表格資料閘道 —— sqlc 產生的就是它
- 資料傳輸物件、對應器 —— 為什麼該是三個型別
- 工具-物件關聯映射模式 —— 整體選擇