跳到內容

ADR-0002 檢核與交接放在應用層,不放在 Stored Procedure

已採納 · 2026-09-09

我維護過的進件流程是這樣運作的:

業務人員在進件頁面按下儲存,前端呼叫一支 Stored Procedure。所有檢核都寫在那支 SP 裡, 全部通過之後,SP 才會往另一張資料表插入一筆——那一筆的存在,就是「這件案子已經進件」 的依據。下游系統看那張表決定要不要接手。

這個做法能動,而且改起來很快:檢核條件要調整,直接改 SP,不用重新部署應用程式。 但用久了之後,有三個問題會浮出來。

SP 不在版本控制裡。檢核條件改了之後,沒有紀錄說明是誰改的、什麼時候改的、為什麼改。 偶爾發生誤改,要復原只能靠記憶,或是某個人手邊剛好留存的舊版本。

「這條規則當初為什麼是這樣」這個問題,在 SP 裡沒有地方可以回答。

T-SQL 沒有好的模組化手段,想複用一段檢核邏輯,最省事的做法就是複製一支新的 SP 出來改。 久了就會有好幾支長得很像但不完全一樣的 SP。

真正的代價不是「檔案變多」,是它們會分岔:本來一樣的兩支,其中一支改了, 另一支沒改。行為從此不一致,而且沒有任何機制會告訴你。要維護時得先花時間確認 這幾支到底哪裡不一樣。

檢核和寫入綁在同一個同步請求裡。SP 的指令一長,執行時間就拉長, 業務人員按下儲存之後會明顯感覺到延遲

而檢核只會越加越多,不會變少。所以這個問題會隨時間惡化。

在這個 API 裡,把三件事分開:

檢核放在應用層,進版本控制。 驗證規則寫在 ApplicationEndpoints.Validate, 每次變更都有 commit、有訊息、有 PR 可以回頭看。

規則只有一份。 允許的狀態轉移集中在 StateMachine 這一個型別裡, 沒有「複製一份改一改」這條路可以走。

交接用顯式契約,不用資料表。 進件的結果是一個 HTTP 回應,不是「某張表多了一筆」。 驗證失敗當場回 400 並列出是哪個欄位、為什麼,而不是靜靜地不插入。

入口只做快速檢核。 POST /v1/applications 只確認資料收得下,立刻回 201, 狀態是 Received。真正的規則判斷延後到批次(ADR-0004)。 使用者不必等那些他等不起的東西。

三個問題各自對應到上面一項,不需要額外解釋。值得多說的是最後一項:

把「使用者等得起的」和「等不起的」分開,是同步長交易唯一真正的解法。 在 SP 裡優化指令可以爭取一些時間,但檢核只會越加越多——這是在跟趨勢對抗。 把重的工作移出請求路徑才是結構上的修正。

  • 改規則要重新部署。 這是實實在在的損失。改 SP 是立即生效的,改應用層要走一次 建置與部署。換來的是可追溯與 review,但「線上出事要馬上調參數」這種場景確實變慢了。
  • 多一層網路邊界。 資料表交接和寫入在同一個交易裡,不會有「插入成功但通知失敗」 這種狀態。改成 HTTP 之後,呼叫端必須處理逾時與重試。
  • DBA 不再看得到全貌。 規則從資料庫移到應用程式碼,原本熟悉 SP 的人失去了 直接檢視規則的能力。這在以 DBA 為中心的團隊是真實的組織成本。

把 SP 納入版本控制。 用遷移工具管理資料庫物件,SP 的變更就進得了 git。 這能解決第一個問題,而且改動範圍比重寫小很多。如果我今天要在原系統上做改善, 我會先做這件事——它成本最低、效益最直接。但它解決不了分岔與同步延遲。

維持 SP 檢核,只把慢的部分改成非同步。 針對第三個問題的最小修正。 在無法大改的系統裡這是務實的選擇,但前兩個問題原封不動。

這篇不是在說「SP 爛、應用層好」。SP 內檢核有它成立的條件,而且不少見:

  • 單一資料庫、單一寫入端。 檢核和寫入在同一個交易裡,一致性是免費的。 改成跨網路的服務反而要自己處理部分失敗。
  • 團隊以 DBA 為中心。 規則放在最熟悉它的人看得到的地方。
  • 變更頻率低。 追溯性的價值和變更頻率成正比。一年改兩次的規則, 沒有版控的痛感遠低於一週改兩次。
  • 多個異質的上游都要寫入同一份資料。 檢核放在資料庫,是唯一保證每個上游 都遵守的位置。放在某一個應用程式裡,其他上游可以繞過。

最後一項特別值得注意:把檢核移到應用層,前提是所有寫入都經過那一層。 這個 API 成立是因為它是唯一入口。在一個有批次匯入、有其他系統直接寫入的環境裡, 這個前提不成立,那麼檢核就該留在資料庫。