ADR-0003 批次逐筆逐欄回報失敗,不只回報成敗
已採納 · 2026-09-09
我維護過的系統之間有每日的資料交換排程,基本上都是用 Stored Procedure 在跑。 平常都很快,不太需要管它。
問題出在出事的時候:常常不知道問題在哪,只能一筆一筆去試。
而最麻煩的一類,是欄位長度或型別轉不過去,其中最常見的是字串超過目標欄位的長度。 上游給的值塞不進目標欄位,或是字串轉不成數字、日期。這類錯誤在 SQL Server 裡的訊息通常只說轉換失敗或會被截斷, 不會告訴你是哪一列、哪一個欄位、原本的值是什麼。於是排查只能靠縮小範圍: 先跑一半,再跑四分之一,直到找出那一筆。
當天的交換量越大,這個過程越久。而且修好那一筆之後重跑,常常又撞上下一筆。
交換匯入的回傳值,把失敗的細節當成一等的輸出,而不是寫進日誌就算了:
{ "runId": "EXCH-20260909120845", "received": 6, "accepted": 2, "rejected": 4, "acceptedIds": ["APP-20260909-00001", "APP-20260909-00002"], "rejects": [ { "rowId": "R005", "errors": [ { "field": "ApplicantRef", "rawValue": "", "reason": "必填,但來源為空值" }, { "field": "Amount", "rawValue": "9999999", "reason": "超出允許範圍 10,000 ~ 2,000,000" }, { "field": "TermMonths", "rawValue": "999", "reason": "超出允許範圍 3 ~ 60" } ] } ]}三個具體的選擇:
一、回傳原始值。 rawValue 帶著上游送來的東西原封不動。排查時最想知道的
就是「它到底送了什麼」,而那正是轉換失敗後最容易遺失的資訊。
二、一列的錯誤全部列出,不在第一個就停。 上面的 R005 有三個欄位都有問題,
一次全部回報。只回報第一個的話,修完重跑會撞第二個,一列要跑三趟。
三、壞的那幾筆不擋住整批。 能收的先收,收不了的逐筆退回。 整批 all-or-nothing 在交換場景幾乎總是錯的——一筆髒資料擋住當天全部的量, 而且隔天重跑還是同一筆擋住。
交換的輸入欄位因此刻意全部宣告為字串。型別轉換是這一層的責任, 不是反序列化的責任。若讓 JSON 反序列化去轉,轉不過去時拿到的是框架的錯誤訊息, 上面三件事一件都做不到。
排查時間的差別是數量級的。原本要靠二分法找出那一筆,現在是回應裡直接寫著。
更重要的是誰能排查。錯誤訊息說得夠清楚時,上游的人可以自己看懂並修正資料; 訊息只說「轉換失敗」時,每次都要維護這支排程的人下去查。這是把責任放回資料來源端。
- 逐筆檢查比整批轉換慢。 每一列都要跑一遍欄位檢查,而不是交給資料庫一次轉完。 以交換的量級來說這通常無所謂,但如果單日是百萬列等級,就得重新衡量。
- 錯誤格式是一份契約。 呼叫端會開始依賴
field與reason的內容。 之後要改文字或結構,等於改介面。 - 回應可能很大。 極端情況下每一列都失敗,回應會膨脹。目前用單次 500 筆的上限
擋住,超過回
413。真正的大批量應該改成回報一個結果檔的位置,而不是塞在回應裡。
考慮過的替代方案
Section titled “考慮過的替代方案”只寫日誌,回應僅回成敗。 最省事,而且日誌裡確實查得到。但這保留了原本的問題: 要有人去撈日誌,而上游看不到。排查依然卡在同一個人身上。
整批 all-or-nothing。 資料一致性最單純,交換要嘛全成功要嘛全失敗。 在必須保證整批原子性的場景是對的,但每日交換通常不是——擋住當天所有資料的代價, 遠高於少收幾筆。
分段重跑,失敗的資料另存一張實體表。 這是我實際看過的做法:出問題時把批次 切成幾段分別重跑,縮小範圍找出問題那一段;失敗的資料另存一張實體表,處理完再刪掉。
它能解決問題,但代價都落在人身上:排查時間隨資料量成長;那張表是臨時開的, 沒有 schema 管理也沒有保留期限,得有人記得刪;而且**「為什麼壞掉」不會跟著資料 一起被保存下來**——表裡只有原始資料,失敗原因仍然要當場從錯誤訊息推論, 過幾天再回頭看就想不起來了。
這篇的決定,本質上是把那份推論的結果變成輸出的一部分,而不是留在某個人的腦袋裡。