四分鐘影片導讀:從使用者的一句話開始,挑了業務員,部門跟著來

影片:https://youtu.be/xY_ZlEAAcoE
影片約 4 分鐘,英文旁白,YouTube 上可以切換英文或繁體中文字幕,以 Polhem 1.12.1 錄製。
前兩篇導讀的是整套流程:從 Excel 匯入定義,到桌面、網頁、手機上執行。這次只談一件事:表單之間的關聯怎麼設計。示範沿用上一支 Blazor Server 影片的 Northwind 專案,在訂單上加入員工與部門。
問使用者一張訂單需要什麼,常聽到的是這樣一句話:
挑業務員,他的部門就跟著出來。他之後調部門,這張單仍歸原部門。
這句話就是整個需求。前半句是操作:挑一個人,相關的資料自己帶出來。後半句是資料的歸屬:訂單記下的是開單當時的部門,不會跟著員工異動。
在一般的系統裡,開發者要先把這句話翻成資料表和外鍵:orders.employee_id 指向 employees,employees.dept_id 再指向 departments。接著要寫三樣東西:
後半句「仍歸原部門」也要另外想清楚:部門要存在訂單上,不能每次都從員工 JOIN 過去,否則員工一調部門,舊單的部門也跟著變。
這些工作每個用到員工的畫面都要再做一次。需求是一句話,實作分散在 SQL、畫面和程式碼裡,事後要從程式碼看出原本的需求並不容易。
Polhem 的關聯定義在表單(FormSchema,描述一張表單有哪些欄位的定義檔)上,內容是「這張表單參照那張表單」,不是「這張表的外鍵指向那張表的主鍵」。欄位上寫的是:挑了這筆,要帶回哪些欄位。這和使用者的說法是同一件事。
影片先打開 DefineEditor(Polhem 的定義檔編輯器)看既有的兩張表單:
每張表單只描述自己直接參照的下一層。要從某張表單讀到更深一層的欄位時,框架會沿著這條鏈自己串出多層 JOIN,宣告的人只需要想一層。
接著在 DefineEditor 打開 Order 表單,新增一個員工欄位,把它指向 Employee 表單。關聯對話框已經替員工的代號和名稱打好勾,這兩個是一般的顯示欄位。
前半句的「部門跟著出來」,是再勾選員工的部門欄位,把它們寫進訂單自己的部門欄位,並標成 LookupOnly(只在挑選時套用):挑選時填入,之後跟著訂單一起保存。
部門欄位本身設成唯讀,因為它一律由員工帶入,不讓使用者另外挑。它仍然參照 Department 表單,訂單才能顯示部門名稱,也能用部門查詢。
存檔後,Order 的定義大致是這樣:
<FormField FieldName="employee_rowid" Caption="Employee" DbType="Guid"
RelationProgId="Employee" ValueChangeCallback="true">
<RelationFieldMappings>
<FieldMapping SourceField="sys_id" DestinationField="ref_employee_id" />
<FieldMapping SourceField="sys_name" DestinationField="ref_employee_name" />
<FieldMapping SourceField="dept_rowid" DestinationField="dept_rowid" LookupOnly="true" />
<FieldMapping SourceField="ref_dept_id" DestinationField="ref_dept_id" LookupOnly="true" />
<FieldMapping SourceField="ref_dept_name" DestinationField="ref_dept_name" LookupOnly="true" />
</RelationFieldMappings>
</FormField>
<FormField FieldName="dept_rowid" Caption="Department" DbType="Guid"
RelationProgId="Department" ReadOnly="true">
<RelationFieldMappings>
<FieldMapping SourceField="sys_id" DestinationField="ref_dept_id" />
<FieldMapping SourceField="sys_name" DestinationField="ref_dept_name" />
</RelationFieldMappings>
</FormField>
讀起來就是使用者那句話:訂單屬於這位員工,也屬於他的部門。幾個值得多看一眼的地方:
RelationProgId="Employee" 指的是 Employee 表單,不是 employees 資料表。LookupOnly 的三筆只在挑選時把值寫進訂單,不參與查詢時的 JOIN。訂單的部門由自己的 dept_rowid 透過下面那段關聯讀回來,所以讀到的是訂單上存的部門,不是員工目前的部門。後半句「仍歸原部門」就是靠這個區分做到的。ValueChangeCallback 是對話框自動勾上的。挑選視窗只回傳員工的代號和名稱,部門不在其中,所以挑選後要請伺服器讀出那位員工的完整資料,把部門填回來。定義改好之後,在 DefineEditor 執行 Sync(同步資料表定義與版面),新欄位就加進資料表定義和表單版面;再按 Upgrade Database,訂單資料表的欄位就升級好了。整個過程沒有寫 SQL,也沒有寫程式。
打開訂單的版面,用網頁預覽看一下:表單上已經有員工,旁邊是唯讀的部門。
最後到 Blazor Server 網站實際操作:

使用者那句話的兩半,在這裡都看得到結果。
這種做法涵蓋的是表單的新增、查詢、修改這類日常操作。報表、統計、批次作業需要任意形狀的 SQL,還是在業務物件裡自己寫。關聯也有限制:JOIN 一律是單一欄位的等值比對(外鍵對主鍵),對象是實體資料表,不支援子查詢或複合鍵。這些在文件的「適用與不適用場景」「限制與設計取捨」兩節有列出來。
用資料表關聯,開發者要把使用者的話翻成 JOIN、挑選視窗和抄欄位的程式;用表單關聯,使用者的話直接寫成定義。好處是需求和實作對得起來:事後打開 Order 的定義,就能讀出「挑員工、帶回部門、部門跟著訂單保存」這件事,每個用到它的畫面行為都一樣。
這是我在 Polhem 裡選擇的做法,適合表單多、欄位和關聯常調整的企業系統。如果你的系統以報表或複雜查詢為主,或已經習慣用 ORM 管理資料模型,不一定需要換成這種方式,可以依專案情況判斷。
想自己試的話,範本用 dotnet new install Polhem.Templates 安裝,再用 dotnet new polhem-blazor-server 建立專案,文件與範例在 GitHub。
📘 HackMD 原文筆記:
👉 https://hackmd.io/@jeff377/polhem-form-relations
📢 歡迎轉載,請註明出處
📬 歡迎追蹤我的技術筆記與實戰經驗分享
Facebook | HackMD | GitHub | NuGet