
「定義是唯一真相」聽起來像是有一份檔案裝著全部真相。實際打開一個專案的定義目錄,看到的是十三種定義類型:有的是根目錄下的單一檔案,有的一支程式一份、散在子資料夾裡。
這一篇是地圖。後面每談一個機制都會用到其中幾份,先在這裡認識,就不必每次停下來解釋。第四節末尾有一張十三份的速查表,標出每一份由哪一天展開,讀到不確定的名字可以翻回這裡。
本篇說明:
SystemSettings / DatabaseSettings / DbCategorySettings
FormSchema / TableSchema / FormLayout
ProgramSettings / MenuSettings / PluginSettings
LanguageResource / PermissionModels / CurrencySettings / UnitSettings
表格第一欄可點進原始碼。
| 定義 | 存法 | 管什麼 |
|---|---|---|
SystemSettings |
單檔 | 主金鑰從哪裡取、payload 怎麼打包與加密、偵錯模式 |
DatabaseSettings |
單檔 | 這個部署有哪些實體資料庫、各自怎麼連上去 |
DbCategorySettings |
單檔 | 哪一張資料表屬於哪一類、該類由哪一個資料庫承載 |
這三份是系統起得來的前提,而且讀取順序固定:
SystemSettings(指名主金鑰在哪裡)
→ 主金鑰解出保護設定用的金鑰
→ 解開 DatabaseSettings 裡的連線密碼
→ DbCategorySettings 寫的資料庫代號才對得到東西
順序不是慣例,是相依性算出來的唯一解。倒過來走的路根本不存在:在資料庫連得上之前,沒有任何機會先去資料庫查一把金鑰。
鏈條的第一份必須在什麼都還沒起來時就讀得懂,所以它自己不能加密。主金鑰因此不放在 SystemSettings 裡面,那份檔案只記「金鑰在哪裡」:
<MasterKeySource>
<Type>Environment</Type>
<Value>BEE_MASTER_KEY</Value>
</MasterKeySource>
框架也據此把啟動時那一次讀取獨立出來:不透過任何服務、不經過快取,直接讀檔,因為在那個時間點其他東西都還不存在。
兩者回答不同的問題:
DatabaseSettings |
DbCategorySettings |
|
|---|---|---|
| 回答什麼 | 這個部署有哪些資料庫、怎麼連 | 哪一類資料落在哪一個資料庫 |
| 性質 | 部署環境的事實 | 應用結構的事實 |
| 什麼時候改 | 搬機房、換資料庫伺服器、測試環境改指向 | 新增一張表、把某批資料換一個承載的資料庫 |
| 敏感度 | 裝連線密碼,要加密、要管存取 | 沒有秘密,開發者天天改 |
分家的好處是兩邊各改各的,順帶把敏感的與日常的隔開。
資料實際上會落在幾個資料庫、跟資料表命名前綴是什麼關係,Day 11 會專門談。
一份記著連線字串的檔案歸在定義層,看起來像分層破了。沒有破,因為它放的是描述,不是連線。
定義層這個套件的引用清單極短,它不知道有資料庫、不知道有 HTTP、不知道有 UI,連線字串在它眼裡只是一段字串加一個代號。真正開連線的是資料存取層:定義層只宣告一個「拿得到 DatabaseSettings」的介面,由它去取用。
| 定義 | 存法 | 管什麼 |
|---|---|---|
FormSchema |
一支程式一份 | 定義中樞:欄位、型別、跨表關連、主檔與明細、計算欄與驗證規則 |
TableSchema |
一張表一份 | 實體資料表的欄位、型別、長度、允不允許 NULL、索引 |
FormLayout |
一張版面一份 | 這些欄位在畫面上怎麼排 |
Day 1 那個「訂單加預計到貨日」要改的三處,就是這三份。
FormSchema 是中樞,另外兩份各自對著一個方向:一個對資料庫,一個對畫面。
FormSchema:宣告有哪些欄位、什麼型別、哪些指向別的表單、主檔配了哪幾張明細、哪些值是算出來的、存檔前檢查什麼。框架拿它組 CRUD SQL、決定資料跨層傳遞的形狀。TableSchema:關心的是實體儲存,不是業務語意,所以長度、索引、精度可以由熟悉資料庫的人單獨調整。FormLayout:三份裡唯一只管畫面的。怎麼排是設計階段的決定,寫成一份版面定義檔,執行期照它排。三份的關係也決定改動順序:FormSchema 描述表單有哪些欄位,改動從它開始;TableSchema 跟著補實體欄位,要顯示才進 FormLayout。
| 定義 | 存法 | 管什麼 |
|---|---|---|
ProgramSettings |
單檔 | ProgId 對到哪個 Business Object 與哪個 Repository,留空就走框架預設 |
MenuSettings |
單檔 | 導覽樹的分組、排序、標題與可見性,每個項目指向一支程式 |
PluginSettings |
單檔 | 每支程式在存檔與刪除的時點要跑哪幾個 plugin,依宣告順序執行 |
ProgramSettings 是一張註冊表,每支程式一列,決定這支程式的請求由誰處理。兩個綁定欄位留空就走框架預設的 CRUD 流程;填了類別名稱,就由那個類別接手。Day 3 說案例八張表單只有訂單要寫程式,看的就是這張表。MenuSettings 跟註冊表分家,是因為讀它的不是同一端:註冊表由伺服端讀,裝的是程式碼裡的類別名稱;選單由前端讀,裝的是排序、標題與可見性。而且同一支程式本來就可能出現在選單的多個位置,所以節點有自己的識別碼,不用 ProgId 當鍵。PluginSettings 記的是每支程式在存檔與刪除前後要跑的那幾個 plugin 與順序。跟繼承改寫的差別 Day 3 提過(繼承是取代、只能一個;plugin 是疊加、可以多個),細節留給 Day 21。| 定義 | 存法 | 管什麼 |
|---|---|---|
LanguageResource |
每種語言一組,再依表單或分類切檔 | 在地化的標題與選項文字 |
PermissionModels |
單檔 | 有哪些權限模型、各自管哪些動作、資料範圍怎麼界定 |
CurrencySettings |
單檔 | 各幣別的小數位與最小流通面額 |
UnitSettings |
單檔 | 各計量單位的顯示小數位 |
這四份不屬於任何一張表單,但每一張表單都會查到它們。
LanguageResource 是逐條疊上去的。FormSchema 裡本來就寫了欄位標題,語系資源提供各語言版本,對得上就換掉,對不上就用原本那個。不需要多語的專案完全不必碰,加了也不會逼你把每一條都翻完。PermissionModels 登錄的是「有哪些權限可以談」。哪些動作需要授權、資料範圍怎麼界定,這些是模型;至於誰被授予了什麼,那是資料不是定義,不在這份檔案裡(Day 24 展開)。前面四節是依用途分組,這裡把十三份攤在一起,順序不變:
| 定義 | 管什麼 | 哪一天展開 |
|---|---|---|
SystemSettings |
主金鑰來源、payload 選項、偵錯模式 | 本篇 |
DatabaseSettings |
這個部署有哪些資料庫、各自怎麼連 | 本篇 |
DbCategorySettings |
哪一類資料落在哪一個資料庫 | 本篇 |
FormSchema |
定義中樞:欄位、型別、關連、主檔與明細、規則與計算欄 | Day 5 |
TableSchema |
實體資料表:欄位、長度、允不允許 NULL、索引 | Day 6 |
FormLayout |
這張表單在畫面上怎麼排 | Day 7 |
ProgramSettings |
ProgId 綁哪一個 Business Object 與 Repository,只給伺服端 | Day 15 |
MenuSettings |
導覽選單的分組、排序、標題與可見性 | Day 15 |
PluginSettings |
每個 ProgId 掛哪些 plugin、依宣告順序執行 | Day 21 |
LanguageResource |
每種語言一組的標題與選項文字 | Day 20 |
PermissionModels |
有哪些權限可以談:動作與資料範圍策略 | Day 24 |
CurrencySettings |
各幣別的小數位與最小流通面額 | Day 26 |
UnitSettings |
各計量單位的顯示小數位 | Day 26 |
這些名字在後面二十幾天會反覆出現,而且一律用原文。中文說法講不清楚是哪一份:
「分類檔」可能是 DbCategorySettings,也可能是 PermissionModels;
「設定檔」十三份裡有八份都叫這個名字。
定義內容是 XML,放在哪裡可以換:磁碟上的檔案,或資料庫裡的一張表(一列一份定義,欄位裡放那段 XML)。框架透過同一個介面去讀,換一種放法,上面每一層都不必知道。
執行的時候用到的不是那段 XML。框架把它讀進來、還原成一個定義物件,之後要查什麼都是問這個物件。XML 只是它躺在磁碟或資料庫時的樣子。
快取裡放的就是這些還原出來的物件,一份定義一格,淘汰看的是使用頻率:被讀到就重新計時,一段時間沒人碰才丟掉。
這是定義切成一份一份的回報之一:快取的單位就是讀取的單位,記憶體用量跟得上實際使用量,而不是跟著定義總量走。
留著就有一件事要交代:源頭被改掉了怎麼辦。每一份快取各自盯著自己的來源,檔案就盯修改時間,資料庫就走另一套通知機制;來源一變,下一次要用的時候重讀一份新的(Day 12 展開)。
從框架取得一份定義,拿到的是共用的那一份,不是複本。同一台伺服器上,所有使用者拿到的是同一個物件。
代價是那份物件從此不能碰:
被禁掉的只有「直接在共用那一份上動手」這一種寫法,而它本來就是錯的,改到的是所有人的東西。
框架內嵌了一組預設定義,但它在執行期完全不參與。框架只讀你指定的定義目錄,內嵌那組是開新專案時的起點,用工具複製進去,一次性動作。
不做回退是刻意的。如果 FormSchema 找不到就自動改用內嵌那份,系統會照樣跑起來,然後用著一份你根本沒放進去的定義。「你忘了部署那個檔案」會被改寫成「系統行為跟你想的不一樣」,而後者要花的追查時間高一個量級。
但「找不到」不是只有一種意思,所以十三種逐一分成兩類:
| 類別 | 行為 | 哪幾份 |
|---|---|---|
| 缺了就是設定錯了 | 直接報錯,訊息指名哪一個檔案 | FormSchema、TableSchema、ProgramSettings、PermissionModels,以及啟動三件組 |
| 缺了是合法答案 | 回一個「沒有」,由呼叫端接手 | FormLayout、LanguageResource(用結構裡的標題)、CurrencySettings / UnitSettings(用預設位數)、MenuSettings、PluginSettings |
這個分類寫在程式裡,而且必須由框架決定,不能讓每個應用各自解讀。否則同一個疏漏在不同模組會有不同表現,一個報錯、一個安靜地用了預設值,那比兩個都報錯難查得多。
Northwind 用到框架提供的部門與員工兩張表單,做法就是上一節那個一次性動作:把預設定義複製進應用自己的定義目錄。
複製之後,應用自己在員工上加了兩個欄位,這兩行就在案例的 Employee.FormSchema.xml 裡:
<FormField FieldName="title" Caption="Title" DbType="String" />
<FormField FieldName="hire_date" Caption="Hire Date" DbType="Date" />
加完之後那份定義就完全歸應用所有,想怎麼改就怎麼改。反過來,框架日後在預設定義上新增的欄位不會自己跑進來,也不會有任何提醒;要不要跟上,自己去改那份檔案。
框架的那幾個系統欄位則不能拿掉,權限與組織的機制認的就是它們。
這是「定義是資料」在維護面上的具體樣子:一份資料複製出去自己維護,跟一支程式分支出去自己維護,要付的東西不一樣。前者不會有合併衝突,也不必逐版把主幹的修正搬回來,代價是新東西不會自動跟上。
十三份的形狀大致是:三份決定系統跑在什麼上面,三份描述一張表單,三份決定誰來執行與從哪裡進來,四份是所有表單都會查的共用資料。
這十三份不是十三個設定檔,是同一個真相的十三個面向。它們彼此指來指去(DbCategorySettings 指向 DatabaseSettings、FormSchema 指向別的 FormSchema、ProgramSettings 與 MenuSettings 各自指向同一支程式),卻不打架,靠的是每一份都只回答自己該回答的那個問題。
明天談 FormSchema 怎麼變成 SQL。
本系列同步發表於 HackMD,完整目錄