iT邦幫忙

2026 iThome 鐵人賽

DAY 10
2

Day9_好的資料庫設計,讓你的程式更有彈性

前言

上一篇認識了 Table、Primary Key 與 Foreign Key,接著要把 Project Management 的需求真正轉成資料表。這一步不能只看畫面有哪些輸入框。畫面會改版,資料的生命週期與業務關係通常更長久;如果直接照著畫面建表,很容易把暫存狀態也存進資料庫,最後留下不少用途不明的欄位。

以工作項目清單為例,checkbox 只用來選取這次要批次更新的資料,重新整理後就會消失,所以它不是 TaskItems 的欄位。相反地,「不再顯示批次確認視窗」需要跟著使用者保留,適合放在 UserPreferences。兩者看起來都出現在前端,生命週期卻完全不同。

先找實體,再決定資料表

我會先從 User Story 找出 Entity(實體)、Attribute(屬性)與 Relationship(關係),而不是先打開資料庫工具新增欄位。

這個專案的主要實體包括帳號、專案、專案成員、工作項目、留言與工作歷程。它們之間的關係如下:

  • 一個專案可以有多個工作項目,一個工作項目只屬於一個專案。
  • 一個帳號可以加入多個專案,一個專案也有多個成員,因此由 ProjectMembers 保存兩者的多對多關係。
  • 一位專案成員可能有多個專案角色,所以再用 ProjectMemberRoles 連接成員與角色。
  • 一個工作項目可以有多筆留言與歷程,留言則各自記錄作者。

多對多關係通常需要中介表。ProjectMembers(ProjectId, AccountId) 作為複合主鍵,資料庫因此能阻止同一個帳號重複加入同一個專案。這比在 Service 裡先查一次「是否已存在」更可靠,因為兩個請求同時送進來時,程式檢查仍可能一起通過,資料庫約束才是最後一道防線。

正規化不是拆表比賽

正規化的目的,是減少重複資料與更新異常。這個專案先以 2NF 作為主要目標,再依一致性與查詢情境判斷是否需要繼續拆分,不會為了追求更高正規化而製造一長串難以理解的 JOIN。

階段 要檢查的事情 專案例子
1NF 每個欄位只保存一個值,不把清單塞進字串 不在 Projects 內存放逗號分隔的成員 ID,而是建立 ProjectMembers
2NF 非鍵欄位必須依賴完整主鍵,不能只依賴複合主鍵的一部分 ProjectMembers 不重複保存帳號名稱;名稱只依賴 AccountId,應由 Accounts 取得
3NF 非鍵欄位不應依賴另一個非鍵欄位 若每個角色代碼都有固定名稱,名稱放在 ProjectRoles,中介表只保存角色 ID

不過,重複不一定都是錯。例如 TaskItemHistoriesSnapshot 保存當時的工作項目內容,目的是保留歷史,而不是拿來取代目前資料。這種有明確業務原因、寫入時機與讀取方式的重複可以接受;真正要避免的是同一份現況散落多處,修改時卻不知道哪一份才算數。

約束把錯誤擋在資料庫外

資料表結構確定後,還要把重要規則寫成資料庫能執行的限制。

設計 在本專案的用途
Primary Key Id 或複合鍵唯一識別資料
Foreign Key 防止工作項目指向不存在的專案或帳號
NOT NULL 要求標題、狀態等必要資料一定要有值
UNIQUE 保證角色代碼、權限代碼或 Token Hash 不重複
CHECK 限制商業編號計數器的類型與數值範圍
最大長度與資料型別 日期用日期型別、狀態限制長度,避免所有欄位都變成無上限字串

ProjectsTaskItems 採軟刪除,程式以 DeletedAt 判斷資料是否仍有效。它們的 Code 使用 filtered unique index,只要求「尚未刪除」的資料不可重複。這個選擇要配合業務規則:如果歷史編號永遠不得重用,就不該排除軟刪除資料。

刪除行為也不能全部設成 Cascade Delete。專案成員屬於專案的一部分,可以在專案被實體刪除時連帶處理;工作項目、留言、歷程與稽核紀錄則可能需要保留,因此目前多採 Restrict 或 NoAction。否則刪掉一個帳號,相關操作紀錄也一起消失,日後很難追查發生過什麼事。

索引要從查詢方式反推

索引可以加快查詢,但會占用空間,也會增加新增與更新資料的成本,所以不是每個欄位都要加。工作項目列表經常依專案、狀態與交付期限篩選,TaskItems 因此建立 (ProjectId, Status, Deadline) 複合索引;查詢指派對象時則使用 AssignedAccountId 索引。留言常依工作項目與建立時間讀取,對應的索引是 (TaskItemId, CreatedAt)

這些索引來自實際查詢,而不是憑感覺建立。功能上線後仍要觀察執行計畫與慢查詢,再決定要新增、調整或移除索引。

同時修改資料時怎麼辦?

多人協作系統很常出現兩位使用者同時編輯同一筆 Task 的情況。TaskItemsProjects 與留言都有 rowversion 欄位;每次資料異動,SQL Server 會產生新的版本值。更新時若版本已經不同,後端便能回報資料曾被其他人修改,避免後送出的請求默默蓋掉前一個人的結果。

要注意,rowversion 是版本用的二進位數字,不是修改日期。若畫面需要顯示「最後更新時間」,仍應另外保存 UpdatedAt

專案的資料庫設計

把前面的判斷放進 Project Management Web 後,目前的資料庫共有 21 張資料表。Day8 先用帳號、專案、專案成員與工作項目說明主要關係,實際開發時還得補上登入授權、專案角色、留言、歷程與系統紀錄。為了讓結構比較容易閱讀,我把 E-R Diagram 分成三個區塊:

區塊 主要資料表 負責的事情
Identity 與 RBAC AccountsRolesAccountRolesFunctionsRoleFunctionsRefreshTokensUserPreferences 保存帳號、系統角色、功能權限、登入 Token 與使用者偏好
專案、成員與 Task ProjectsProjectMembersProjectRolesProjectMemberRolesTaskItemsTaskItemCommentsTaskItemHistories 保存專案的主要業務資料,以及成員、工作項目、留言和異動歷程
系統紀錄 BusinessCodeCountersAuditLogsEmailMessages 產生 Project/Task 編號,記錄資料異動與 Email 寄送結果

第一個區塊處理「這個人是誰,以及他能做什麼」。AccountsRoles 透過 AccountRoles 連接,再由 RoleFunctions 決定角色具有哪些功能權限。UserPreferences 則是一對零或一的關係,因為帳號可以還沒有建立偏好設定;這也呼應前言提到的批次確認選項,它屬於使用者設定,不是 Task 本身的資料。

https://ithelp.ithome.com.tw/upload/images/20260908/20126487LYBtRORYUw.png

第二個區塊是專案功能的主體。Projects 透過 ProjectMembers 連接參與專案的帳號,成員再經由 ProjectMemberRoles 取得一個或多個專案角色。這裡刻意把「系統角色」與「專案角色」分開:某個帳號在系統中可能只是一般使用者,但在特定專案裡可以同時擔任後端工程師與系統分析師。

TaskItems 一定屬於一個 Project,也會記錄建立者與被指派的帳號。每個 Task 可以有多筆 TaskItemCommentsTaskItemHistories;留言保存討論內容,歷程則保存操作者、動作與當時的 JSON 快照。即使 Task 後來又被修改,系統仍能回頭查到先前發生過的事情。

https://ithelp.ithome.com.tw/upload/images/20260908/20126487CjsuR8U8Xl.png

最後一個區塊沒有硬塞進專案主線。BusinessCodeCounters(CodeType, BusinessDate) 複合主鍵管理每天的 Project/Task 流水號;AuditLogs 可以記錄帳號操作,也允許 ActorAccountId 為空,保留系統自動執行的紀錄。EmailMessages 只保存收件者與寄送狀態,目前沒有對 Accounts 建立 Foreign Key,因為寄信紀錄的生命週期不必依附帳號資料。

https://ithelp.ithome.com.tw/upload/images/20260908/20126487DIf40mKWT9.png

E-R Diagram 適合先看全貌,確認資料表之間是一對一、一對多,還是透過中介表形成多對多;Table Schema 則用來核對欄位型別、Null、PK、FK、索引與刪除規則。兩份文件要一起看,才不會只知道資料表有連線,卻不知道那條線受到哪些約束。正式 Schema 仍以 EF Core migrations 為準,文件的用途是讓設計更容易閱讀,也方便前後端討論資料契約。

走到這裡,Day8 提到的關聯式資料庫概念已經變成可以實作的 Schema。下一步就是把資料與商業規則包在後端 API 裡,再交給 Vue 前端使用,這也正好接到 Day10 的前後端分離。

小結

好的資料庫設計不是資料表越多越好,而是每張表的責任清楚,關係能表達業務,重要規則也有約束保護。Project Management 的 Schema 從 User Story 出發,把短暫的畫面狀態留在前端,把需要長期保存的專案、成員、工作項目、偏好與歷程交給資料庫。之後畫面或 API 改版時,核心資料不必跟著全部重做,程式才有調整的空間。

下一篇會沿著這個基礎,說明 Vue 前端與 ASP.NET Core 後端如何透過 API 分工,又該怎麼守住彼此的資料契約。

參考資料


上一篇
Day8_要怎麼把資料記錄下來?用資料庫來保存紀錄吧
下一篇
Day10_我的架構不只是給網頁用,來談談什麼是前、後端分離
系列文
Codex的規格驅動開發 :30 天打造 .NET 內部專案管理系統14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言