🎯 什麼情境該想到我
當你「想讓一個物件對應資料表中的一列,作為存取該列的閘道」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:讓每個物件對應資料來源中的一筆記錄(一列),物件的欄位對應該列的欄位,並提供存取該列的方法。
做法要點:
- 一個實例 = 一列;欄位映射到資料庫欄位,透過 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)。要配樂觀鎖。
🔗 相關工具
- 表格資料閘道 Table Data Gateway(以「整張表」為單位的閘道對照)
- 主動記錄 Active Record(在列物件上再加業務邏輯)
- 資料對應器 Data Mapper(更徹底解耦的替代方案)
- 企業應用架構模式
- 表格資料閘道 —— ★ Go 的做法
- 主動記錄 —— 加上業務邏輯之後
- 資料對應器 —— 完全解耦的做法
- 識別對應 —— 解決同一列多個物件
- 記錄集 —— 更早期的做法
- 樂觀離線鎖 —— 防 lost update
- 工具-打破依賴以便測試 —— 遺留系統的封裝過渡