iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Software Development

ERP 架構師筆記:定義驅動的框架設計系列 第 3

Day 3:Northwind 案例的八張表單與自訂業務物件

  • 分享至 

  • xImage
  •  

Day 3:Northwind 案例的八張表單與自訂業務物件

前兩天談的都是命題,今天讓案例登場。

案例是 Northwind,微軟那套從 Access 年代流傳到現在的範例資料庫。選它的理由很現實:客戶、供應商、產品、訂單這幾張表,多數人不需要解釋就知道是什麼,於是可以直接談這個結構考驗了框架什麼,不必先講這個業務在做什麼。

本篇說明:

  1. 案例要能考驗框架的四個條件
  2. 八張表單的三種形狀,與框架的系統欄位
  3. 關連為何宣告在表單之間,不是資料表之間
  4. 唯一寫程式的那一張,以及它為什麼還配了一個 Repository
  5. 那個 Business Object 裡裝了什麼
  6. 框架管標準流程,應用只接手它管不到的那一小段

先看做出來的樣子。這是八張表單裡最複雜的訂單,同一份定義在四個前端上跑出來的畫面。

桌面 瀏覽器
Northwind 訂單表單,桌面 Northwind 訂單表單,瀏覽器
iOS Android
Northwind 訂單表單,iOS Northwind 訂單表單,Android

主檔配一張明細,客戶、員工、貨運商三個欄位都是開窗挑一筆回來,明細每列再挑一項產品,金額不給人填。四個前端共用同一個 UI 專案、讀同一份定義,沒有一張畫面有對應的畫面程式碼,差別只在最外面的那一層平台外殼。


一、案例要能考驗框架什麼

框架的示範幾乎都長一樣:一張單表,欄位排一排,增修刪查會動。這種示範什麼都證明不了,因為所有框架都做得到。真正會壞的地方,一個都不在那張示範上。

一個能當驗證用的案例,至少得同時具備四個條件:

  • 表單要有難有易:純主檔、帶跨表關連的主檔、主檔配明細的單據都要有。只在最簡單那一種上成立的框架,就只是一個表單產生器。
  • 關連不能只有一種走法:一張表指向另一張是基本款,還要有兩張表互相指向、應用的表指向框架自己的表,才逼得出關連機制怎麼設計。
  • 要有一張得加入自訂邏輯的表單:宣稱「大部分不用寫程式」的框架,案例裡若一行程式都沒有,那不是證明主張,是迴避主張。
  • 要跑在多個前端上:版面定義號稱不綁 UI 框架,同一份定義沒有真的長出兩種以上畫面,這句話就不算數。

Northwind 四項都撐得住。它的複雜度落在一個好位置:夠複雜到能考驗機制,又夠小到能整套攤開來逐項對帳。


二、八張表單,三種形狀

形狀 張數 表單
純主檔(欄位、清單、CRUD,無關連無規則) 4 分類、供應商、客戶、貨運商
帶跨表關連的主檔 3 產品(→ 供應商、分類)、部門(→ 員工,作為經理)、員工(→ 部門)
主檔配明細的單據 1 訂單

部門與員工互相指向,是刻意留的環狀關連。

八張表單對應九張資料表(訂單佔兩張)。九張裡有兩張是框架提供的表:部門與員工。權限與組織機制會用到它們,應用把定義複製過來直接用(框架的表與應用的表怎麼區分、各自落在哪個資料庫,Day 11 會專門談)。

這九張表不是 Northwind 原本的形狀

借的是業務案例與示範資料,不是資料表結構。原本的 Northwind 拿業務欄位當主鍵(CustomerID='ALFKI'、整數產品編號、訂單明細的複合主鍵),這套應用一個都沒沿用。

換上的是框架的系統欄位,而且框架的機制就掛在這幾個名字上:

欄位 框架拿來做什麼
sys_rowid 跨表 JOIN、UPDATE / DELETE 的 WHERE、稽核、資料範圍權限、前端點選一列,比對的都是這一欄
sys_master_rowid 明細怎麼撈、整筆怎麼一起存
sys_no 主鍵與預設排序
sys_id 建表時自動配唯一索引,業務代碼的唯一性由此擔保
sys_name 開窗選取時顯示的那一欄

所以這幾個名字不能改,這不是命名慣例的問題。上面那幾件事,框架全靠它們去認。

兩個設計決定與它們的代價:

  • 主鍵與關連鍵分開:主鍵是自動遞增的 sys_no,關連鍵是 Guid 型的 sys_rowid。關連鍵要不會變(代碼改了,舊單據不能斷),也要在寫進資料庫之前就存在(整筆訂單一次存,明細得先指到主檔)。代價是多一個唯一索引,鍵也比較胖。
  • 明細不用複合主鍵。主檔明細的形狀若各家不一樣,整筆存取的機制就無法對所有表單通用。

還有一項代價比較重:既有系統要接進來得先改資料表結構,不是把定義寫一寫就能跑在原本的資料庫上。


三、關連宣告在表單之間,不是資料表之間

傳統做法裡關連是資料表的事:訂單表放一個客戶欄位,加一條外鍵指到客戶表主鍵。這條宣告能表達的剛好就是字面那麼多,這一欄的值必須在那一欄裡找得到。

框架把關連挪到另一個層次:

<FormField FieldName="customer_rowid" Caption="Customer" DbType="Guid" RelationProgId="Customer">
  <RelationFieldMappings>
    <FieldMapping SourceField="sys_id" DestinationField="ref_customer_id" />
    <FieldMapping SourceField="sys_name" DestinationField="ref_customer_name" />
  </RelationFieldMappings>
</FormField>

RelationProgId="Customer" 指的是「客戶」這支程式,不是 ft_customer 這張表。整段宣告從頭到尾沒有出現表名,要 JOIN 到哪一張表,是框架拿這個代號去查客戶那份 FormSchema 才知道的。

指向表單和指向資料表,能接上的東西差很多:

  • 一張資料表只知道自己有哪些欄位
  • 一份 FormSchema 還知道:清單顯示哪幾欄、開窗挑選時給人看什麼、哪些欄位能查詢、它自己要不要再加條件(例如只列啟用中的供應商)

所以寫完上面那一段,四件事同時到位:

  1. 這個欄位在畫面上自動變成開窗選取的編輯器
  2. 選中一筆之後客戶代號與名稱自動填回
  3. 查詢清單時框架自己組出 JOIN 把這兩欄撈回來
  4. 資料庫存的是關連鍵

傳統做法這四件分記在三個地方(外鍵在資料庫、選取畫面設定在前端、JOIN 在查詢層),三處都可能對不起來。

資料庫裡沒有外鍵

框架不建外鍵約束,訂單那三個關連欄位在資料庫裡只有索引。

這是把關連挪到定義層的必然結果:關連的權威在定義層,資料庫只是它投影出來的產物。真要在資料庫也建一份外鍵,那份宣告就有兩個來源,而兩個來源遲早分歧。

代價:資料庫不會替你擋孤兒資料。有人直接下 SQL 刪掉一個客戶,指向它的訂單不會被攔下來。願不願意接受,取決於你的資料庫是不是只有這個應用在寫。「定義是唯一真相」這個假設要一路貫徹到資料庫層,不能一半交給定義、一半交給約束。

顯示欄不是權威值

ref_customer_idref_customer_name 這兩個欄位在資料表裡並不存在。

  • 訂單實體表只放客戶的關連鍵
  • 代號與名稱是查詢時由伺服器 JOIN 出來的
  • 前端開窗挑完會把值寫進手上那份資料,只為了畫面立刻看得到,存檔時不會落進資料庫
  • 客戶改了名字,訂單上顯示的值下次讀取就跟著對

典型取捨:每次查詢多一次 JOIN,換到的是畫面上永遠是現在的值。代價是要有一個明確的規定說「哪一邊是權威」,而這個規定必須由框架來定,不能讓每張表單各自決定。

這套應用宣告了幾處

實測八處:訂單主檔三處(客戶、員工、貨運商)、訂單明細每列一處(產品)、產品兩處(供應商、分類)、員工一處(部門)、部門一處(經理)。

Day 1 說的「三處跨表關連」是訂單主檔上的那三處,後面對帳用的也是這個範圍。

這八處裡有一處跨過一條線:訂單指向的員工是前面那兩張框架表之一。訂單上的業務員就是框架的員工,應用不必再建一張自己的員工表。

環狀關連為什麼留著

部門指向員工(誰是經理)、員工指向部門,在資料模型上是一個環。刻意留著,因為凡是「從定義產生東西」的機制遇到環都要有明確處理方式:

  • 建置期產生強型別類別 → 環會逼出型別初始化順序問題
  • 執行期依定義動態組查詢 → 環只影響那一次查詢要 JOIN 幾層

這正是 Day 1「產生的是程式碼還是物件」在資料模型上的投影。


四、訂單:唯一寫了程式的那一張

八張表單裡七張的應用程式碼是零行。註冊表上這七張只寫了 ProgId 與顯示名稱,代表走框架預設流程。只有訂單那一列指定了兩個型別:

<ProgramItem ProgId="Order" DisplayName="Orders"
             BusinessObject="...OrderBO, Bee.Northwind.Server"
             Repository="...OrderRepository, Bee.Northwind.Server" />

兩個型別各管一段:Business Object(以下簡稱 BO)放這張表單的業務邏輯,Repository 放它的資料存取。

那個 Repository 存在的理由只有兩句 SQL:讀出訂單目前存在資料庫裡的狀態,以及查出本月最大的訂單編號。其餘 CRUD 仍由框架依定義組出來。

把這兩句放進 Repository 而不是 BO,不只是分層潔癖:BO 得自己指名連哪一個資料庫,Repository 則由 FormSchema 的分類加上目前的 session 解析出來,自動走到正確那一家公司的資料庫,寫的人不必知道系統裡有幾家公司。


五、那個 Business Object 裡裝了什麼

訂單的 BO 覆寫兩個方法,存檔之前那一步做了四件事:

  1. 檢查至少要有一列明細
  2. 把明細金額加總寫回主檔總計
  3. 產生訂單編號
  4. 檢查狀態轉換是否合法

四件看起來都像「業務邏輯」,但拿 Day 2 那條判別法(全世界的 ERP 做起來是不是都一樣)去切,只有最後一件是。

前三件都是通則。單據至少要有一列明細、總計等於明細加總,這是約定俗成的做法;訂單編號是前綴加年月加流水號,這種取號原則多數 ERP 都做成可設定的機制。三件應該都能收斂為機制,它們現在寫在應用裡是因為框架還沒收。

最後一件才是業務判斷。狀態從草稿到確認到出貨,不能跳階、不能回頭,確認後明細鎖住。這條規則每一家都不一樣(有些公司確認後還能改數量,有些多兩個中間狀態),該由應用寫。

那條界線移動過

BO 一開始做的不只這四件,有兩批東西已經搬出去了:

  • 明細每列的金額以前在程式裡算,現在是欄位上的一行運算式
  • 客戶與產品必填、數量大於零這三條檢查以前在程式裡,現在是 FormSchema 裡的三條規則
<FormField FieldName="amount" DbType="Currency" NumberKind="Amount" ReadOnly="true"
           ValueExpression="quantity * unit_price * (1 - discount)" />

搬過去之後省的不只是那幾行:運算式在使用者改動數量或折扣的當下就重算,於是前端不必為了「金額要即時跳動」再寫一次同樣的計算。同一個算式,一處宣告,兩端都算得出來。

還有一件走得更遠:能改它的人也跟著換了。金額怎麼算現在寫在定義裡,要調整就是改一份定義檔,不必回到程式碼那一側。每有一件事從程式搬進定義,動得了它的人就往需求端靠一步:原本得排進開發排程的東西,變成懂業務的人看得懂、也改得動的一行算式。

所以「哪些該寫程式」這條線不是一開始畫定的,它會隨框架的宣告能力往前推。剛才那四件裡,前三件遲早會被收走,剩下的只有最後那一件。

留在程式碼裡的東西,一部分本來就該在那裡,一部分只是還沒被接管。混在一起看不出差別,分開看才知道下一步該收哪一個。


六、框架管標準流程,應用只接手它管不到的

框架管的是每張表單都一樣的事:查清單、開一筆、新增、修改、刪除、開窗挑選、存檔前驗證、把資料送到前端。這些事的形狀不隨表單改變,會變的只是欄位有哪些,而欄位就寫在定義裡。控管也一樣:誰能存取、哪些資料看得到、每次異動是誰在什麼時候改了什麼。

所以那七張表單,應用一行程式都沒寫。

講到這裡通常會冒出一個疑慮,而且是該冒出來的:流程都標準化了,那彈性呢?

這一題比「框架能做多少」重要得多。訂標準不難,難的是訂完之後還留不留得住例外。留不住,第一次碰到特殊需求就得整條流程自己刻,前面省的全部還回去。案例裡碰到這件事的就是訂單那一張。

接手有兩種方式:

方式 機制 性質
繼承後改寫 框架的 BO 把整條流程切成一連串可覆寫的步驟(取一筆新資料、存檔之前、存檔本身、存檔之後,刪除同樣切法),應用只改需要的那幾步。訂單只動了兩步 取代,一支程式一個
掛外掛 框架在存檔與刪除的前後開了四個時點讓外部邏輯掛進去 疊加,可疊多個,客製那條鏈接在套裝那條後面,不動原程式

兩種方式的共同點才是對那個疑慮的回答:接手的是流程裡的一小段,不是整條流程。覆寫「存檔之前」不會讓你連 SQL 怎麼組、資料怎麼送到前端都得自己來。

彈性不在於框架什麼都做得到,而在於你只需要處理跟機制做法不同、或機制還沒支援的那一部分。標準流程與例外能不能共存,看的就是這個顆粒度:切得太粗,例外一出現就得整條重寫;切得夠細,標準的部分就繼續替你做。

回到 Day 1 那句話

Day 1 說:整套 Northwind 做完,八張表單裡只有一張需要自己寫程式。那一張就是訂單,而它需要接手不是因為比較複雜,是因為它碰到了框架現行機制管不到的那幾件。案例需要有這樣一張表單,否則「大部分不用寫程式」這句話等於沒被驗證過。

而處理它們,覆寫框架切出來的那兩步就夠了,不必離開那條標準流程。這個空間是必要的,機制不能為了把流程標準化就把彈性收掉。ERP 裡的特例有很多不是需求寫壞了,是那家公司的組織與作業慣例本來就長那樣,不會因為框架訂了標準就消失(框架把接手的深度切成幾段,Day 30 會整個攤開)。

這就是 Definition-Driven 想達到的狀態:不是不用寫程式,是只在真正需要判斷的地方寫,而且寫的時候只接手那一小段。

明天開始進定義層,先把十三種定義檔逐一交代清楚。


本系列同步發表於 HackMD,完整目錄


上一篇
Day 2:Definition-Driven 架構總覽
下一篇
Day 4:定義檔的用途與分類
系列文
ERP 架構師筆記:定義驅動的框架設計4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言