iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Software Development

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

Day 5:FormSchema 如何驅動 SQL

  • 分享至 

  • xImage
  •  

Day 5:FormSchema 如何驅動 SQL

昨天把定義檔逐一攤開,FormSchema 是其中的定義中樞。今天問一個具體的問題:一張表單的查詢與存檔,那幾句 SQL 是從哪裡來的。

本篇說明:

  1. 新增一張表單要動的六個地方(零行程式碼)
  2. 查詢:JOIN 清單與翻頁,都在每次請求當下決定
  3. 存檔:欄位集合怎麼變成 INSERT / UPDATE / DELETE
  4. 它為什麼不是 ORM,以及少掉的那些東西為何不是缺漏
  5. 一千份定義與一千個類別的差別

一、多一張表單,要動六個地方

Day 1 數的是「既有表單加一個欄位」。這裡數的是整張表單都是新的:

要動什麼 動作
FormSchema 新增一份:欄位、關連、明細、規則
TableSchema 每張實體資料表新增一份:欄位、長度、索引
FormLayout 新增一份:這些欄位在畫面上怎麼排
DbCategorySettings 加一列:這張表歸哪一類、由哪一個資料庫承載
ProgramSettings 加一列:ProgId 與顯示名稱
MenuSettings 加一項:使用者從哪裡點進來

三份新檔案、三行新增、零行程式碼,主檔配了明細就多一份 TableSchema。真正要從頭寫的只有 FormSchema,另外兩份可以從它推導出來,再各自調整。案例八張表單裡有七張就是這樣長出來的。

六個地方沒有一個是重述,各自回答不同問題:這張表單有什麼、實體儲存長什麼樣、畫面上怎麼排、歸哪個資料庫、誰來執行、從哪裡點進去。

既然一行程式都沒寫,按下查詢、按下存檔跑的是什麼?同一份 FormSchema 同時撐起兩件事,用到的部分完全不同:

  • 查詢用的是關連宣告,組出 JOIN
  • 存檔用的是欄位集合,組出 INSERT / UPDATE / DELETE

二、查詢:JOIN 是查出來的,不是寫出來的

Day 3 講過關連宣告在表單之間,那份宣告沒有出現表名。框架拿它做的事很短:

RelationProgId(程式識別碼)
  → 取那支程式的 FormSchema
    → 取出主表
      → 取出實體資料表名
        → JOIN 接在該表的 sys_rowid 上

多繞這一層的回報有兩個。

一個在寫的時候:宣告的是「訂單指向客戶」,不是「ft_order 的某一欄指到 ft_customer 的主鍵」。關連定在資料表之間時,寫的人得先把業務上那個關係翻成儲存層的樣子;定在表單之間,寫下來的就是腦子裡的那件事,中間少一次翻譯。

另一個在改的時候:實體表名只寫在那張表單自己的結構裡,別的表單指過來寫的是 ProgId。客戶那張表要改名,改的是客戶那一份定義,所有指向它的表單都不必動。表名若逐處寫死,這件事就是一次全域搜尋,而且沒有人能擔保搜乾淨了。

JOIN 清單是每次現算的

訂單清單顯示六個欄位,框架組出來的查詢是:

SELECT A.[sys_id], A.[order_date],
       B.[sys_name] AS [ref_customer_name], C.[sys_name] AS [ref_employee_name],
       A.[status], A.[total_amount]
FROM [ft_order] A
LEFT JOIN [ft_customer] B ON A.[customer_rowid] = B.[sys_rowid]
LEFT JOIN [st_employee] C ON A.[employee_rowid] = C.[sys_rowid]

訂單主檔宣告了三處關連,這一句只 JOIN 兩張表。清單沒有要顯示貨運商,那張表就不會進來。

規則只有一條:

框架先把這次查詢會碰到的欄位收成一份清單,來源有三個(要撈出來的欄位、條件裡出現的欄位、排序用的欄位);清單裡出現了哪些關連欄,就 JOIN 哪幾張表。

所以「用到」不等於「顯示」:

  • 拿客戶名稱過濾清單 → 客戶表為了 WHERE 被 JOIN 進來,即使顯示欄沒有它
  • 開單筆畫面 → 三個 JOIN 都在,因為三欄都要顯示
  • 只挑主檔自己的欄位 → 一個 JOIN 都沒有

條件的值一律走參數,不進 SQL 字串。

沒有任何地方存著「訂單要 JOIN 哪幾張表」這個答案。代價是把「這句 SQL 長什麼樣」從建置期移到每一次請求,每次查詢多一段組語句的工作。它是這條路線的固定開銷,不是可以最佳化掉的東西。

同一段宣告在畫面那一側也在做事:欄位設了關連,畫面上那一格就是開窗選取的編輯器,從來源那張表單挑一筆帶回來。JOIN 讀的宣告和編輯器讀的宣告,是同一段。

顯示欄沒有落在資料表裡

上面那句 SQL 的 ref_customer_name 是一個別名,訂單實體表只有 customer_rowid

  • 前端開窗挑完會把代號與名稱寫進手上那份資料,只為了畫面立刻看得到
  • 存檔時這幾欄不進資料庫,下次讀取由伺服器 JOIN 重新帶出來
  • 權威值永遠在查詢那一側

於是不會有「客戶改了名字,舊訂單顯示的還是舊名字」。代價是每次顯示都得 JOIN 一次。

哪一種對,看這個欄位要的是「當時的值」還是「現在的值」:顯示用的名稱要現在的,成交當下的單價要當時的,而後者本來就該是訂單自己的欄位。這條界線得由框架先畫好,否則每張表單各自決定,同一個系統裡會出現兩種行為。

單階宣告,多階執行

員工表單除了帶出部門名稱,還帶出主管姓名。它的關連欄指向部門,其中一條對映的來源欄寫的是 ref_manager_name,而那一欄在部門表單上也是一個關連欄(指向員工,誰是經理)。於是要往下再走一層:

FROM [st_employee] A
LEFT JOIN [st_department] B ON A.[dept_rowid] = B.[sys_rowid]
LEFT JOIN [st_employee] C ON B.[manager_rowid] = C.[sys_rowid]

st_employee 出現兩次,A 是員工本人,C 是他的主管。

每張表單只宣告自己直接指向的那一層,而那一層就是業務上原本的說法,寫定義的人不必是程式開發人員也讀得懂。視野固定是一層,框架沿著表單鏈遞迴展開,SQL 深度不設限。這在上千張表單的系統裡是決定性的:關連會織成一張很大的圖,但沒有人需要在腦中裝下它。

部門那一側改了自己的關連,員工這張表單一個字都不必改,下一次查詢自己就多接或少接一層。宣告的人只描述業務上的那一層關係,接得多深、接幾張表,是框架的事。

環狀關連在這裡不是問題

Day 3 留的那個環(部門 → 員工 → 部門),上面那段 SQL 已經走完了。

它不構成問題,因為查詢是每次現算的。環在這裡最多只剩一件事要決定:這一次要 JOIN 到第幾層,而層數由請求的欄位決定,不由模型的形狀決定。沒有人要主管姓名的時候,那一層根本不會出現。

換成建置期產生強型別類別,這個環會變成型別彼此引用、初始化順序怎麼排的問題。同一個環,在執行期衍生的世界裡只是一次查詢的深度,在建置期產生的世界裡是一個得先解掉的結構問題。

翻頁也是每次現算的

翻到第幾頁、一頁幾筆、要不要算總筆數,都由呼叫端送上來,寫在同一個物件上:

var result = bo.GetList(new GetListArgs
{
    SelectFields = "sys_id,order_date,ref_customer_name",
    SortFields = [new SortField("order_date", SortDirection.Desc)],
    Paging = new PagingOptions { Page = 2, PageSize = 20 },
});

框架會先把這幾個值整理過再用。頁次與頁大小夾在合理範圍內,回來的 result.Paging 是真正生效的那一組。總筆數預設不算,多撈一筆就知道後面還有沒有,要精確數字才多下一句 COUNT。分頁一定要有排序,沒指定就補上主檔的流水號欄,連那一欄都沒有才要求呼叫端自己講清楚。


三、存檔:欄位集合就是增修刪的語句

框架拿主檔(以及每張明細)的欄位集合現場生出一份資料表描述,增修刪三句都由它產出。訂單的 UPDATE:

Update [ft_order] Set
[sys_id]=@sys_id, [order_date]=@order_date, [customer_rowid]=@customer_rowid, ...
Where [sys_rowid]=@sys_rowid

這句 UPDATE 裡有四件事:

  • 流水號那一欄不在裡面,由資料庫自己配
  • 顯示欄也不在,前一節說過它們沒有實體欄位
  • 條件固定是 sys_rowid,DELETE 同樣如此
  • 異動整列所有欄位,不是只挑改動過的那幾個

第四件用明細來看最清楚。一張單子有十列明細,使用者改了一列、刪掉一列,送出去的是整份資料,真正下到資料庫的只有兩句,其餘八列連碰都不會碰。而那句 UPDATE 會把改過那一列的欄位全部寫一次,不是只寫變動的那一欄。

主檔明細一起送進來時,所有異動包在同一個 transaction,而存檔與刪除的順序剛好相反:

  • 存檔:主檔先、明細後
  • 刪除:明細先、主檔後

框架不需要知道這張單據在業務上是什麼,只需要知道哪一張是主檔、哪些是明細、明細靠哪一欄掛回主檔。

還有一件事,名字很容易誤導:這三句都是從 FormSchema 生出來的,不是從 TableSchema 讀出來的。TableSchema 管的是實體資料表怎麼建、長度與索引怎麼調、資料庫怎麼跟著定義升級,那是明天的題目。


四、它為什麼不是 ORM

這一節不是在說 ORM 不好,也不是在比兩者的優缺點。ORM 在它被設計的場景裡運作良好,問題出在數量(Day 1 講過)。

差別不在功能多寡,在對映的兩端各是什麼:

ORM 這套機制
應用那一側 類別與屬性 一份執行期的結構描述
資料庫那一側 資料表與欄位 資料表與欄位

起點不同,長出來的東西自然不同。少掉的有三樣。

沒有實體類別

查詢結果不會被還原成物件,資料以列的形式帶著結構走。既然沒有類別,就沒有「多一張表要多一個類別」,Day 1 那個乘以一千的問題在這一層直接不成立。代價是編譯期型別檢查跟著沒有了(Day 9 談怎麼補)。

沒有以屬性表達的關連,所以沒有延遲載入

ORM 裡「訂單的客戶的業務員」這樣一路走下去的寫法在這裡沒有對應物。要哪些欄位在查詢當下就得講明,於是不會有「不小心多碰了一個屬性、每一列各打一次資料庫」的意外。反過來,臨時想看一個沒宣告過的欄位,得先去定義裡加一條對映。

變更追蹤沒有不見,它換了位置

ORM 靠一個上下文物件握著載入過的實體,存檔時比對哪些屬性變了。這裡不必比對:每一列自己就記著它是新增、改過、刪掉還是沒動過,改過的欄位連舊值都留在上面。狀態記在列上、沒有記到欄位,上一節那句 UPDATE 才會寫整列。

這三件不是做一半,是這套機制裡本來就沒有它們的位置。另外還有幾條限制,性質不一樣:JOIN 一律是單欄位等值、接的一定是實體資料表,子查詢與非等值接法都不在裡面。這些不是做不到,是這條路只管制式表單那一段。報表、統計、批次匯入走另一軌,SQL 自己寫(Day 11 展開)。


五、一千份定義與一千個類別

上千張業務表在物件對映模式下就是上千個類別,外加各自的 DTO、對映設定與 Repository。這些要編譯、要維護、要隨資料庫結構同步,而同步沒有工具能完全代勞,因為它牽涉到「這個欄位在業務上是什麼」。

同樣上千張表,在 Definition-Driven 下是上千份同構的資料。差別不在數量,在誰能整批處理它們。

一份定義是結構化資料,直接後果有三個:

  • 批次腳本掃得動:「所有金額欄位的小數位數是不是都設對了」是一次全域搜尋加一次判斷
  • 工具產得出來:TableSchemaFormLayout 都是從 FormSchema 推導出來的,那是一段轉換程式,不是一次程式碼產生再加一次編譯
  • AI 接得上:要它照既有十份的體例補出第十一份,它讀的是一份已經把關連與規則宣告好的文件,不是一堆要從程式碼推論意圖的類別

檢查的時機也可以往前挪。一千份定義一樣會寫錯(指到不存在的表單、對映到不存在的欄位、規則裡打錯欄位名),差別在這些是建置期用程式就掃得出來的錯,而框架也確實做了一部分(Day 9 會談)。

一千份錯的定義跟一千個錯的類別一樣糟,定義不會因為它是資料就自動正確。換到的是修正的方式:改一份資料,而不是改一段程式碼再走一次編譯、打包與部署。

定義也沒有離開原本那套工程管理。它們跟程式碼放在同一個原始碼樹裡,一樣進版控、一樣看得到每次改了哪幾行、一樣要在合併之前經過審閱。改的方式變了,管的方式沒有。

還有一項不在成本表上:一千份定義的體例是同一套。看得懂一張表單怎麼宣告關連就看得懂全部,接手的人不必為了每個模組各摸一遍當年那位工程師的習慣。在維護期十年二十年的系統上,人換過幾輪、模組也加過幾批,這一份工每次都得再付一次,維護成本就是這樣疊上去的。


六、回到訂單那張表單

案例裡最複雜的訂單,在定義上是一份 Order.FormSchema.xml:主檔配一張明細、主檔三處關連、明細每列一處、三條驗證規則與一個計算欄(這兩樣怎麼求值是 Day 8 的題目)。多重關連、主檔明細、明細逐列開窗選取,全部宣告出來,沒有一行 UI 或 CRUD 程式碼。

而它同時是那張需要寫程式的表單。那個 Repository 存在的理由只有兩句 SQL(讀資料庫裡的訂單狀態、查本月最大單號),宣告表達不了就自己寫。制式的那一段由定義驅動,剩下那兩句不必因為框架而繞路,繼承一個 Repository、補上去就是了。

一張表單能不能不寫程式,不取決於框架有多聰明,取決於它有沒有把「每一張表單都一樣的那部分」認出來、收斂成同一套機制。查詢要 JOIN 哪些表、存檔要送哪幾句,正好都落在那一部分裡面。

明天談 TableSchemaFormSchema 怎麼變成它,以及實體資料庫怎麼安全地跟著定義升級。


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


上一篇
Day 4:定義檔的用途與分類
下一篇
Day 6:TableSchema 與資料庫結構升級
系列文
ERP 架構師筆記:定義驅動的框架設計6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言