🎯 什麼情境該想到我

當你要設計「物件如何對應到資料庫」,或在不同 ORM 風格間抉擇、遇到 ORM 效能問題時。

⚙️ 怎麼用(認識底層模式)

  1. Active Record:物件自己包含資料庫存取邏輯(user.save())。簡單、快速,適合邏輯不複雜的 CRUD(Rails、Eloquent)。
  2. Data Mapper:物件與資料庫完全分離,由 Mapper 負責搬運。乾淨、可測、領域物件不依賴 DB,適合複雜領域(Hibernate、Prisma、TypeORM 的 mapper 風格)。
  3. Unit of Work:一次交易內追蹤所有變更,最後統一提交/回滾 → 減少 DB 往返、保證一致。
  4. Identity Map:同一物件一次請求只載入一次,避免重複查詢與不一致。

★ 根本問題:阻抗不匹配(impedance mismatch)

物件模型關聯模型
巢狀、有繼承、有參考★ 平的表格、只有外鍵
以「身分」識別(指標)★ 以「值」識別(主鍵)
沒有集合操作★ 集合導向(JOIN、GROUP BY)
一次操作一個物件★ 一次操作一批列(這是它快的原因)

★ ORM 就是在翻譯這兩者之間的落差,而所有 ORM 的痛苦都來自「翻譯有損」。

★ Active Record vs Data Mapper

// ★ Active Record:物件自己知道怎麼存
user.Email = "new@x.com"
user.Save()                      // ★ 領域物件依賴資料庫
 
// ★ Data Mapper:物件完全不知道資料庫存在
user.ChangeEmail("new@x.com")    // 純領域行為
repo.Save(ctx, user)             // ★ 由外部的 mapper 負責搬運
Active RecordData Mapper
上手★ 快慢
樣板程式★ 少多
領域物件可測性⚠️ 需要 DB 或 mock★ 純記憶體可測
適合★ CRUD、表結構≈物件結構★ 複雜領域、模型與表差很多
例子Rails、Eloquent、GORMHibernate、Prisma、SQLAlchemy(classical)

★ 選擇判準和 工具-領域邏輯組織模式 一致:
Transaction Script → Active Record;Domain Model → Data Mapper。 兩者要配對,混用會很怪。

★★ N+1 查詢:ORM 最常見也最貴的問題

orders := repo.FindAll()              // ① 1 次查詢
for _, o := range orders {
    fmt.Println(o.Customer.Name)      // ★ 每一筆都觸發一次查詢 → N 次
}
// ★ 100 筆訂單 = 101 次查詢 = 101 次網路往返

★ 它的致命之處是「在開發環境完全看不出來」(10 筆資料時很快),
上線之後資料量一大就爆。

四種修法:

方法做法
★ Eager loadingPreload("Customer") / include —— 一次 JOIN 或兩次查詢帶回
★ DataLoader(批次 + 快取)把同一輪的 N 個查詢合併成 WHERE id IN (...)。GraphQL 生態的標配
直接寫 SQL需要什麼查什麼
★ 偵測在測試或開發環境開「查詢計數斷言」——超過 N 次就讓測試失敗

★ 最後一項最重要:N+1 是「可以被自動偵測」的,不要靠人工 review。

★ 另外兩個核心模式

Unit of Work(工作單元)

uow := NewUnitOfWork(db)
uow.Register(order)                  // 追蹤變更
uow.Register(inventory)
if err := uow.Commit(ctx); err != nil { ... }   // ★ 一次交易、統一提交

★ 價值:(1) 一次交易邊界 (2) 減少往返(批次寫入)(3) 寫入順序由 UoW 決定(避免外鍵順序問題)。

Identity Map(身分映射)

同一個請求內,同一個 ID 只載入一次,回傳同一個物件實例。
★ 價值:避免重複查詢,也避免「同一筆資料在記憶體裡有兩個版本」導致的詭異 bug。

★ Go 的實務選擇(和 Java/Ruby 生態很不一樣)

工具定位
database/sql + 手寫 SQL★ Go 社群的主流。明確、可控、沒有魔法
sqlc★ 從 SQL 產生型別安全的 Go 程式碼——寫 SQL 但拿到編譯期檢查。近年最受推薦的做法
sqlxdatabase/sql 的薄封裝(掃描到 struct)
GORMActive Record 風格。方便但魔法多、效能陷阱多、N+1 容易發生
entschema-as-code + 產生程式碼,偏 Data Mapper

★ Go 社群偏好「明確的 SQL + 程式碼產生」而非「執行期的 ORM 魔法」,
理由和 Go 的整體哲學一致:可讀、可預測、沒有隱藏的成本。
sqlc 是這個哲學的代表——它讓你同時擁有 SQL 的控制力與型別安全(見 工具-型別推導還是解耦 的同源思路)。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意 / 什麼時候不適用

  • ★★ N+1 是最常見的效能殺手(見上)。在 CI 加查詢計數斷言。
  • ★ ORM 隱藏了成本。user.Orders 這一行可能是一次 JOIN、也可能是 100 次查詢——
    從程式碼上看不出來。 → 一定要開 SQL log 看實際發出的查詢。
  • ★ 惰性載入 + 交易邊界外 = 爆炸。交易關閉後才存取 lazy 欄位 → 錯誤或額外查詢
    (Java 的 LazyInitializationException 是經典)。
  • 複雜查詢不要硬用 ORM。報表、分析、多表聚合 → 直接寫 SQL。
    ★ 「全部都用 ORM」和「全部都手寫 SQL」都是教條,混用才務實。
  • ORM 的遷移功能要小心。自動產生的 migration 可能鎖表或重寫整張表(見 工具-資料編碼與演進)。
  • 批次操作要用批次 API。迴圈裡 save() 一萬次 = 一萬次往返。
  • 不要讓 ORM 決定你的領域模型。為了 ORM 方便而把領域物件變成貧血的資料袋,是本末倒置。

🔗 相關工具