第3章封面圖:Section 03 MSSQL資料模型與EFCore

這章要學會什麼

  • 說出 13 張業務資料表分別屬於「服務規格」「審核案件」「使用權」「營運紀錄」哪一群,各自回答什麼問題。
  • 解釋為什麼「使用申請」跟「使用權」要分成兩張表,並用資料庫規則保證一件申請最多只會產生一筆使用權。
  • 設定資料表的欄位長度、關聯、唯一性限制,並看得懂資料庫回報的錯誤訊息是哪一條規則擋下了壞資料。
  • 用工具從零開始建出整個資料庫(20 張資料表),並產生一份可以重複執行的建置腳本。
  • 匯入示範資料,並用一條查詢串出某支服務的版本、審核、申請、使用權與呼叫紀錄之間的關聯。

先備知識

要先完成第 2 章(分層架構、環境檢查頁確認 SQL Server 已連線但資料庫「尚未建立」)。要看得懂基本的資料庫查詢語法,知道什麼是主鍵、外鍵。建議先裝好資料庫圖形管理工具,方便直接檢視資料表內容。

觀念白話講

為什麼資料要能「追溯」,不能只存「目前的狀態」

如果把「服務狀態」「審核結果」「誰能用」全部塞在同一張表的幾個欄位裡,新值一寫進去、舊值就消失了。之後管理者想問「這支服務當初是誰核准的?理由是什麼?為什麼昨天還能呼叫、今天卻不行?」——資料庫會答不出來。

所以這個平台的資料設計原則是:「會改變的狀態」跟「已經發生過的事件」分開存放,事件只會不斷新增,不會被覆蓋掉。

13 張業務資料表,分成四群

群組回答的問題
服務規格這支服務是誰的、有哪些版本、呼叫上游要帶什麼憑證、映射成哪個 AI 工具
審核案件誰在什麼時候送了什麼申請、誰在什麼時候用什麼理由核准或退回
使用權現在誰能用哪個版本、今天用了幾次、外部身分對應到哪位會員
營運紀錄每一次呼叫的結果與耗時、每一次管理操作的軌跡

另外還有 7 張表是會員系統框架附帶產生的(帳號、角色這類),合計 20 張。

為什麼「使用申請」跟「使用權」要分成兩張表

從使用申請到呼叫紀錄留下的足跡:①服務版本已發布→②會員送出使用申請→③管理者的審核紀錄→④建立使用權→⑤每次呼叫都新增一筆呼叫紀錄。每一步都是新增一列,沒有任何一步覆蓋前一步的資料。
圖 3-1 從使用申請到呼叫紀錄留下的足跡:①服務版本已發布→②會員送出使用申請→③管理者的審核紀錄→④建立使用權→⑤每次呼叫都新增一筆呼叫紀錄。每一步都是新增一列,沒有任何一步覆蓋前一步的資料。

「審核紀錄」跟「使用權」看起來很像,卻是兩件不同的事:審核紀錄記的是「當初核准的決定」,永遠不會改變;使用權記的是「現在能不能用」,會因為被撤銷、到期、額度用完而失效。所以「王小明當初有沒有被核准」要看審核紀錄,「王小明現在能不能呼叫」只要看使用權這一張表就好,不必回頭翻歷史。撤銷授權時只改使用權那一列的狀態,審核紀錄完全不動——這就是「申請審核狀態」跟「授權有效狀態」分開保存的實際做法。

會員系統的關聯,跟「刪除規則」

會員資料表由現成的會員系統框架提供,本課程額外加了顯示名稱、是否停用、建立時間三個欄位。這個框架相關的類別放在資料層、不放在業務規則層——因為它是框架附帶的東西,放進業務規則層就違反了「業務規則層什麼都不參考」的規矩。

所有外鍵關聯都設定成「不能串連刪除」:假如刪一支服務會連帶刪光它的所有版本,再刪版本又連帶刪光申請跟使用權,歷史紀錄就整組不見了。設定成「不能串連刪除」會讓「刪除仍被引用的資料」這個動作直接失敗,逼著大家改用「下架」或「停權」,而不是真的刪除。

用資料庫本身的規則擋住壞資料

程式難免會有 bug,所以最後一道防線放在資料庫層級本身:

  • 唯一性限制擋住:同一件使用申請被核准兩次而產生兩筆使用權、同一支服務出現兩個相同版號、同一天同一份使用權被算錯次數、兩個版本搶同一個 AI 工具名稱。
  • 條件約束擋住:一筆審核紀錄同時(或都不)屬於「上架審核」跟「使用申請」這兩種案件類型——正常情況下只能屬於其中一種。

這些規則就算哪天程式邏輯漏檢查,資料庫本身也會拒絕寫入不合規則的資料。

「版本號」與「UTC 時間」的兩個小機制

版本號欄位:資料庫可以讓某個欄位在每次該列被更新時自動變成一個新的遞增值。這個機制讓程式能判斷「我看到的資料,跟現在資料庫裡的是不是同一個版本」——如果兩位管理者同時打開同一件待審案件,第一位核准後這個值會改變,第二位再按核准時系統會發現「版本號對不上」,因此不會被核准兩次。這個機制會在第 6 章正式派上用場。

時間一律存 UTC:所有時間欄位都存世界標準時間,不存本地時間。理由是伺服器可能部署在不同時區,畫面顯示時才轉換成臺灣時間;儀表板的「今天」也是依臺灣時區去算日期邊界,不受伺服器所在時區影響。

用工具建出整個資料庫的過程

從 C# 模型到資料庫資料表:①寫好實體類別與設定→②比對目前模型與上一次的差異,產生一份「異動腳本」→③這份腳本可以先被檢視、審閱→④真正執行這份腳本去改資料庫→⑤資料表建好,同時在一張「已套用清單」裡留下紀錄。
圖 3-2 從 C# 模型到資料庫資料表:①寫好實體類別與設定→②比對目前模型與上一次的差異,產生一份「異動腳本」→③這份腳本可以先被檢視、審閱→④真正執行這份腳本去改資料庫→⑤資料表建好,同時在一張「已套用清單」裡留下紀錄。

整個過程分成「產生異動腳本」跟「真正套用到資料庫」兩個步驟,中間可以先審閱腳本內容再決定要不要套用。資料庫也不是網站啟動時自動建出來的,一定要透過這個「套用」的動作才會真的改資料庫——這是刻意的設計,避免不小心對正式資料庫做出非預期的結構變更。

老師示範做了什麼

示範情境:從「資料庫完全不存在」開始,用一條指令建出全部 20 張資料表,啟動網站自動匯入一批示範資料,在後臺看到各表的筆數與服務之間的關聯;接著故意寫入兩筆違反規則的資料,讓大家親眼看到資料庫本身怎麼把它們擋下來。

後臺「資料庫概況」頁:上方顯示已套用的建置腳本紀錄;左側列出每張資料表目前的筆數(會員、服務定義、版本、審核紀錄、使用權、呼叫紀錄……);右側可以選一支服務,即時查出它的版本、上架申請、使用申請、有效使用權、呼叫紀錄各有幾筆。
圖 3-3 後臺「資料庫概況」頁:上方顯示已套用的建置腳本紀錄;左側列出每張資料表目前的筆數(會員、服務定義、版本、審核紀錄、使用權、呼叫紀錄……);右側可以選一支服務,即時查出它的版本、上架申請、使用申請、有效使用權、呼叫紀錄各有幾筆。

老師接著故意對資料庫直接送出兩筆壞資料:第一筆讓同一筆審核紀錄「同時」指向一件上架申請跟一件使用申請,被條件約束擋下並回報明確的錯誤代碼;第二筆想替同一件使用申請建立第二筆使用權,被唯一性限制擋下。兩次都失敗後,資料表的筆數完全沒有增加——證明資料庫層的防線是真的在運作,不是裝飾用的。

自己動手的步驟

步驟一:加入資料庫套件與建置工具

在資料層加入連接資料庫用的套件,在網站專案加入「設計階段」需要的套件,並把建置工具本身的版本也鎖住(寫進一份工具清單檔案,別人在別台電腦上還原時會裝到同一版)。

步驟二:定義實體類別與資料表細節

在業務規則層寫出所有實體類別(服務定義、版本、憑證、工具映射、兩種申請、審核紀錄、使用權,以及呼叫紀錄與稽核紀錄等),這些類別本身只是單純的屬性容器。真正的資料表細節(欄位長度、關聯、哪個欄位要唯一、要不要建索引)寫在資料層各自獨立的設定類別裡:

// 版本設定範例(節錄自 ApiVersionConfiguration)
e.Property(x => x.Version).HasMaxLength(20);
e.Property(x => x.RowVersion).IsRowVersion();               // 標成「版本號」欄位
e.HasIndex(x => new { x.ApiDefinitionId, x.Version }).IsUnique(); // 同一支服務的版本號不能重複
e.HasIndex(x => x.Status);                                  // 公開目錄常依狀態查詢,建個索引加速

沒有特別設定長度上限的文字欄位(例如存放 JSON 規格的欄位),會變成「不限長度」的類型,剛好適合存放長度不固定的規格內容。

審核紀錄的設定則示範了「條件約束」跟「一件申請只能有一筆使用權」:

// 一筆審核紀錄,兩個關聯欄位恰好只能有一個不是空的
e.ToTable(t => t.HasCheckConstraint("CK_ApprovalRecords_ExactlyOneRequest",
    "([PublicationRequestId] IS NOT NULL AND [AccessRequestId] IS NULL) OR "
  + "([PublicationRequestId] IS NULL AND [AccessRequestId] IS NOT NULL)"));

// 使用權:同一件申請最多只能核准出一筆
e.HasIndex(x => x.SourceAccessRequestId).IsUnique();

步驟三:接上網站設定,產生並套用建置腳本

在啟動設定裡把「要連哪個資料庫」讀出來、註冊給整個網站使用;接著執行指令,先產生一份異動腳本(此時還不會真的動到資料庫),確認沒問題後再套用:

dotnet ef migrations add InitialCreate --project src/McpPlatform.Infrastructure --startup-project src/McpPlatform.Web
dotnet ef database update --project src/McpPlatform.Infrastructure --startup-project src/McpPlatform.Web

套用成功後,資料庫裡會出現全部 20 張資料表,加上一張「已套用清單」的紀錄表。

步驟四:換上真正的資料層實作,並匯入示範資料

資料層的真正實作跟第 2 章記憶體版的假資料實作,遵守的是同一個介面——所以呼叫端(服務層、控制器)完全不用改,只要在啟動設定裡把「要用哪個實作」的那一行換掉就好。查詢時要記得只選需要的欄位、投影成跨層傳遞的資料格式,確保像憑證這類敏感欄位根本不會出現在公開查詢的結果裡。

示範資料匯入程式只在「開發環境」而且「資料庫已經建好、沒有還沒套用的異動」時才會執行,執行過一次之後重複執行不會再匯入第二次。

常見錯誤

症狀原因怎麼處理
建置工具指令找不到專案設定只指定了「異動腳本放哪裡」,沒有指定「從哪個專案讀取連線設定」兩個參數都要給:異動檔案放的專案,以及啟動用的網站專案
違反條件約束的錯誤訊息(代碼 547)審核紀錄同時(或都沒)指向兩種案件類型這是資料庫在正常運作,檢查寫入程式邏輯,兩個欄位只能填一個
違反唯一性限制的錯誤訊息(代碼 2601)同一件使用申請要再核准出第二筆使用權這也是資料庫在正常運作,不是缺陷
用某些命令列工具查中文資料變成亂碼該工具輸出編碼跟終端機顯示編碼不一致改用圖形化管理工具查詢,或調整終端機的顯示編碼

怎麼驗收+反向驗證

  1. 可以從空白資料庫建出結構:另外建一個全新的驗收用資料庫,套用同一套建置腳本,資料表筆數應該是 20 張(不含紀錄表),而且裡面完全沒有資料——建置腳本只建結構,不放資料。驗收完把這個臨時資料庫刪掉。
  2. 查得出服務的完整關聯:執行一段只讀查詢,串出某支服務的版本、上架審核決定、使用申請人、使用權狀態與額度、呼叫次數與成功次數,路徑正好對應圖 3-1 的資料足跡。
  3. 反向驗證:重新執行一次教師示範裡的違規寫入,必須再度得到同樣的錯誤代碼,而且審核紀錄的筆數完全沒有增加——如果資料庫接受了這筆壞資料,代表條件約束沒有真的建立成功,要回頭檢查建置腳本內容。
← 上一章 回課程地圖 下一章 →