🎯 什麼情境該想到我

當你「想讓一個物件對應資料表中的一列,作為存取該列的閘道」的時候。

⚙️ 怎麼用(步驟 / 公式)

意圖:讓每個物件對應資料來源中的一筆記錄(一列),物件的欄位對應該列的欄位,並提供存取該列的方法。

做法要點:

  • 一個實例 = 一列;欄位映射到資料庫欄位,透過 getter/setter 存取。
  • 物件只做資料存取,不含業務邏輯(業務邏輯放別處)。
  • 通常搭配一個 Finder 物件負責查詢並產生列閘道實例。
  • 與主動記錄的差別:列資料閘道刻意「不」放業務邏輯。

★★ 它和主動記錄只差一件事:有沒有業務邏輯

// ★ 列資料閘道:一個物件代表一列,只有存取,沒有業務規則
type OrderGateway struct {
    ID         string
    CustomerID string
    Status     string
    Total      int64
 
    db DBTX     // ★ 未匯出
}
 
func (g *OrderGateway) Insert(ctx context.Context) error
func (g *OrderGateway) Update(ctx context.Context) error
func (g *OrderGateway) Delete(ctx context.Context) error
func FindOrder(ctx context.Context, db DBTX, id string) (*OrderGateway, error)
 
// ★★ 加上這個,它就變成 [[主動記錄]]:
func (g *OrderGateway) CanBeCancelled() bool { return g.Status == "pending" }

★ 這條界線在實務上極難守住。 一旦物件上有欄位又有方法,
下一個人一定會把「順手的」業務判斷加上去——而那是漸進的,沒有任何一次 review 會擋下來。
★ 所以 Fowler 自己也說:如果你打算放業務邏輯,就直接承認它是主動記錄。

★★ 在 Go 裡這個模式幾乎不用,理由很具體

// ★ 問題一:物件持有 db → 從此它不是純資料
type OrderGateway struct { … ; db DBTX }
// ★★ 後果:
//   ① 無法安全地在 goroutine 間傳遞(db 可能是一個已 rollback 的 tx)
//   ② 無法直接 json.Marshal(雖然未匯出欄位不會被序列化,但語義上它不是 DTO)
//   ③ ★ 測試要 mock 一個 DBTX 才能建物件
 
// ★ 問題二:Go 的慣例是「資料是資料、行為是函式」
//   ✅ Go 式:
row, err := q.GetOrder(ctx, id)            // ★ 純資料 struct([[表格資料閘道]])
err = q.UpdateOrderStatus(ctx, params)     // ★ 操作是函式
//   → ★★ 沒有人需要「一個知道怎麼存自己的 struct」

★★ 結論:Go 用 表格資料閘道(sqlc 的 Queries)取代了它,
而且那個方向更好——純資料 struct 可以自由傳遞、序列化、比較、當 map key。

★ 它還有意義的兩個場景

① ★ 遺留系統的封裝
   已有一堆散落的 SQL → 先包成列資料閘道,讓存取有單一入口
   → ★ 這是 [[工具-打破依賴以便測試]] 的一步,不是終點

② ★ 腳本/一次性工具
   一個資料遷移腳本裡「載入一列、改幾個欄位、存回去」
   → ★ 這時「物件自己會存」確實方便,而且沒有長期維護成本

★★ 如果你在別的語言看到它:認出兩個退化方向

★ 退化方向一:加業務邏輯 → 變成 [[主動記錄]]
   → 領域複雜時,持久化與業務邏輯耦合會拖累維護

★ 退化方向二:★★ 每個方法各自撈資料 → 變成 [[表格模組]] 的問題
   g := FindOrder(ctx, db, id)      // 查詢 1
   g2 := FindOrder(ctx, db, id)     // ★ 查詢 2 —— 同一列兩個物件,改了會互相覆蓋
   → ★ 這正是 [[識別對應]] 要解決的問題,而列資料閘道本身沒有解

★★ 第二個問題是這個模式最大的隱患:
沒有識別對應時,同一列在記憶體裡可以有多個物件,
而它們各自 Update() → 後寫的蓋掉先寫的,而且沒有任何錯誤。

★ 兄弟辨析

差別
表格資料閘道★ 一個型別服務整張表,回傳純資料 struct;★ Go 的做法
主動記錄★ 列資料閘道 + 業務邏輯
資料對應器★ 領域物件完全不知道資料庫存在
識別對應★ 解決「同一列多個物件」的問題
記錄集更早期的做法:一個泛用的表格結構

🧪 我實際套用的紀錄

  • (待填)

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

  • 若想把業務邏輯也放進去,那其實是走向主動記錄。

  • 需要領域物件與資料庫互不知情時,改用資料對應器。

  • 以整表為操作單位時,改用表格資料閘道。

  • ★★ 在 Go 裡幾乎不用這個模式。物件持有 db 讓它不能自由傳遞、不好測;Go 的慣例是「純資料 struct + 操作函式」(表格資料閘道)。

  • ★★ 「不放業務邏輯」這條界線守不住。一旦物件有欄位又有方法,下一個人一定會加判斷。打算放就直接承認是 主動記錄。

  • ★★ 沒有 識別對應 時,同一列可以有多個物件,各自 Update() 會互相覆蓋且沒有錯誤。

  • ★ 物件持有的 db 可能是一個已 rollback 的 tx。跨 goroutine 或跨請求傳遞這種物件是危險的。

  • 它在遺留系統封裝與一次性腳本裡仍然合理,但那是過渡手段,不是終點。

  • Update() 通常會寫回所有欄位,這會覆蓋別人剛改的欄位(lost update)。要配樂觀鎖。

🔗 相關工具