# 從 ORM 到 DR-Map:ERP 系統資料關聯的進化之路 ![dr-map](https://hackmd.io/_uploads/rkIxAwiyZe.png) ERP 系統往往面對高度變動的流程與資料結構。傳統以強型別類別綁定資料表的 ORM(Object Relational Mapping)模式,對於「版本快速迭代」與「客製化需求」顯得僵化。BeeNET 採用 **DR-Map(Definition Relational Mapping)** 模型,以「定義(Definition)」為核心,在執行階段動態建立資料關聯,形成 **Definition-Driven Architecture(定義驅動架構)**,可在不重新編譯程式的情況下即時調整欄位與關聯。 - NuGet:<https://www.nuget.org/packages/Bee.Db> - GitHub:<https://github.com/jeff377/bee-library> --- ## 1️⃣ DR-Map 與 ORM 的比較 | 面向 | ORM(Object Relational Mapping) | DR-Map(Definition Relational Mapping) | |------|----------------------------------|--------------------------------------| | 關聯來源 | 強型別類別 | FormDefine 結構化定義(XML) | | 綁定方式 | 編譯期 | 執行期動態綁定 | | 模型更新 | 需重新編譯程式 | 更新定義即可生效 | | 擴充性 | 限於程式碼層 | 支援外部配置與快取重載 | | 適用場景 | 固定資料結構 | 多變動態表單與企業系統 | > 💡 ORM 注重「類別與資料表」的靜態映射;DR-Map 注重「定義與關聯」的動態生成,讓系統能在執行階段依定義調整資料結構。 --- ## 2️⃣ DR-Map 架構設計 BeeNET 的 DR-Map 架構將「資料表結構、欄位、關聯」集中於 **FormDefine** 定義檔中。每個 FormDefine 描述一個表單的資料模型,包括欄位(Field)、關聯(Relation)、參照(Reference)。透過 **RelationProgId** 屬性,FormDefine 可以描述與其他表單的關聯。系統於執行階段解析這些定義後,會動態生成 SQL 指令,實現靈活的資料查詢與關聯運作。 ```mermaid graph TD A["🧠 接收查詢請求(含 Select / Where / Order 條件)"] B["📄 載入主表單 FormDefine(例如 Project)"] C["🔍 解析查詢條件與排序欄位"] D["🧩 根據實際需求建立 FormDefine 關聯樹(Relation Graph)"] E["⚙️ 由 IFormCommandBuilder 動態產生 SQL 指令"] F["💾 執行 SQL 並回傳查詢結果"] A --> B B --> C C --> D D --> E E --> F ``` --- ## 3️⃣ DR-Map 的表單關聯理念 與傳統 ORM 操作「資料表 (Table) 關聯」不同,BeeNET DR-Map 操作的是「表單 (Form) 關聯」。 | 對比面向 | 傳統 ORM | BeeNET DR-Map | |-----------|-----------|-------------| | 關聯層級 | 資料表(Table-Level) | 表單(Form-Level) | | 關聯目標 | 關心資料表的外鍵與欄位 | 關心表單與表單之間的邏輯關係 | | 設計觀點 | 資料庫導向 (Database-Centric) | 業務導向 (User/Process-Centric) | | 關聯深度 | 多層遞迴(Table → Table → Table) | 永遠單階(Form → Form) | | 使用思維 | 理解欄位結構 | 定義要取回的表單欄位即可 | 在 DR-Map 架構中,每個表單(Form)代表一個完整的業務資料集合(如「專案」、「部門」、「員工」)。關聯永遠為「單階 (One-Level)」,開發者不必理解被關聯表單的內部結構,只需描述「表單與表單之間」的邏輯關聯以及要取回的欄位。 例如,專案表單(Project)可關聯到「員工(Employee)」作為專案經理,而員工又可關聯到「部門(Department)」作為其所屬單位。 📄 **FormDefine 範例連結:** - [Department.FormDefine.xml](https://github.com/jeff377/bee-library/blob/main/samples/Define/FormDefine/Department.FormDefine.xml) - [Employee.FormDefine.xml](https://github.com/jeff377/bee-library/blob/main/samples/Define/FormDefine/Employee.FormDefine.xml) - [Project.FormDefine.xml](https://github.com/jeff377/bee-library/blob/main/samples/Define/FormDefine/Project.FormDefine.xml) ```xml <FormDefine ProgId="Project"> <FormTable TableName="Project" DbTableName="ft_project" DisplayName="專案"> <Fields> <FormField FieldName="pm_rowid" RelationProgId="Employee"> <RelationFieldMappings> <FieldMapping SourceField="sys_name" DestinationField="ref_pm_name"/> <FieldMapping SourceField="ref_dept_name" DestinationField="ref_pm_dept_name"/> </RelationFieldMappings> </FormField> </Fields> </FormTable> </FormDefine> <FormDefine ProgId="Employee"> <FormTable TableName="Employee" DbTableName="ft_employee" DisplayName="員工"> <Fields> <FormField FieldName="dept_rowid" RelationProgId="Department"> <RelationFieldMappings> <FieldMapping SourceField="sys_name" DestinationField="ref_dept_name"/> </RelationFieldMappings> </FormField> </Fields> </FormTable> </FormDefine> <FormDefine ProgId="Department"> <FormTable TableName="Department" DbTableName="ft_department" DisplayName="部門"> <Fields> <FormField FieldName="manager_rowid" RelationProgId="Employee"> <RelationFieldMappings> <FieldMapping SourceField="sys_name" DestinationField="ref_manager_name"/> </RelationFieldMappings> </FormField> </Fields> </FormTable> </FormDefine> ``` > 💬 使用者只需思考:「專案要顯示專案經理與部門名稱」,而不需理解員工或部門的底層欄位結構。這種「表單導向 (Form-Centric)」設計,讓 DR-Map 關聯模型更貼近業務語意,開發直覺、維護輕鬆。 --- ## 4️⃣ 核心運作原理 ### 🧩 FormDefine 定義檔 存放於 `samples/Define/FormDefine/` 目錄中,包含表單結構(Table)、欄位(Field)、關聯設定(RelationProgId)、參照欄位(RelationField)。系統在執行階段讀取 FormDefine 後,會將其載入至記憶體快取中,以減少重複解析與 I/O 開銷。 ### ⚡ FormDefine 快取機制 FormDefine 採用 **使用頻率導向(frequency-based caching)** 與 **檔案相依(file-dependent caching)** 的複合快取策略。當某個 FormDefine 被載入後,系統會根據下列原則自動管理其快取生命週期: 1. **檔案相依性(File Dependency)**:若來源 FormDefine 檔案有任何異動(例如欄位調整或關聯更新),快取會立即失效並重新載入最新版本。 2. **存取相對時閾(Relative Expiration)**:每當 FormDefine 被使用,其快取的有效期限會自動延長。換言之,**使用頻率愈高的定義,就會被快取愈久**。 3. **動態回收策略**:長時間未被使用的定義會自動從快取中釋放,以維持系統記憶體效能。 此設計可確保 **常用定義持續存在於快取中**,而變更過的定義能即時更新,達到「即時性」與「穩定性」兼具的效果。 > 💡 由於 FormDefine 屬於定義性資料,變更頻率低、讀取頻率高,因此這種快取機制能顯著降低重複解析成本並提升整體系統效能。 ### ⚙️ IFormCommandBuilder 介面 `IFormCommandBuilder` 定義以 FormDefine 為基礎的 SQL 建構介面,負責動態產生 Select/Insert/Update/Delete 語法。針對 SQL Server,提供 `SqlFormCommandBuilder` 實作,執行 `BuildSelectCommand()` 方法,系統會依據 Select、Where、Sort 條件,自動判斷所需 JOIN 結構,生成對應 SQL。 ```csharp /// <summary> /// 建立 Select 語法的資料庫命令。 /// </summary> /// <param name="tableName">資料表名稱。</param> /// <param name="selectFields">要取得的欄位集合字串,以逗點分隔欄位名稱,空字串表示取得所有欄位。</param> /// <param name="filter">過濾條件。</param> /// <param name="sortFields">排序欄位集合。</param> DbCommandSpec BuildSelectCommand(string tableName, string selectFields, FilterNode filter = null, SortFieldCollection sortFields = null); ``` --- ## 5️⃣ 實際範例:Project → Employee → Department 關聯 以下示範 `BuildSelectCommand()` 在不同查詢條件下如何依據 **FormDefine** 自動決定 JOIN 結構。 這些案例對應單元測試 `BuildSelectCommandTests`,涵蓋從單純查詢到多重參考的完整範圍。 --- ### 🧩 案例一:僅查詢主檔欄位(無關聯) ```csharp var builder = new SqlFormCommandBuilder("Project"); var command = builder.BuildSelectCommand("Project", "sys_id,sys_name"); ``` > 📘 說明: > 僅選取 `Project` 主檔欄位,不包含任何參考欄位,因此不產生 JOIN。 **產生 SQL 範例:** ```sql SELECT A.[sys_id], A.[sys_name] FROM [ft_project] A ``` --- ### 🧩 案例二:依參考欄位過濾(Where 條件) ```csharp var filter = new FilterCondition("ref_pm_name", ComparisonOperator.StartsWith, "張"); var command = builder.BuildSelectCommand("Project", "sys_id,sys_name", filter); ``` > 📘 說明: > 雖然僅選取主檔欄位,但 Where 條件使用了 `ref_pm_name`(專案經理姓名),系統會自動 JOIN 員工表(ft_employee)。 **產生 SQL 範例:** ```sql SELECT A.[sys_id], A.[sys_name] FROM [ft_project] A LEFT JOIN [ft_employee] B ON A.[pm_rowid] = B.[sys_rowid] WHERE B.[sys_name] LIKE @p0 ``` --- ### 🧩 案例三:依參考欄位排序(Order By 條件) ```csharp var sortFields = new SortFieldCollection(); sortFields.Add(new SortField("ref_pm_dept_name", SortDirection.Asc)); var command = builder.BuildSelectCommand("Project", "sys_id,sys_name", null, sortFields); ``` > 📘 說明: > 雖然查詢欄位僅來自 `Project`,但排序欄位使用 `ref_pm_dept_name`,因此會自動加入與員工表(ft_employee)及部門表(ft_department)的 JOIN。 **產生 SQL 範例:** ```sql SELECT A.[sys_id], A.[sys_name] FROM [ft_project] A LEFT JOIN [ft_employee] B ON A.[pm_rowid] = B.[sys_rowid] LEFT JOIN [ft_department] C ON B.[dept_rowid] = C.[sys_rowid] ORDER BY C.[sys_name] ASC ``` --- ### 🧩 案例四:同時選取多個參考欄位 ```csharp var command = builder.BuildSelectCommand("Project", "sys_id,sys_name,ref_owner_dept_name,ref_pm_dept_name"); ``` > 📘 說明: > 若選取欄位包含多個參考欄位(例如專案負責人部門與專案經理部門),DR-Map 會依據各欄位的 RelationProgId 自動建立多層 JOIN。 **產生 SQL 範例:** ```sql SELECT A.[sys_id], A.[sys_name], B.[sys_name] AS [ref_owner_dept_name], D.[sys_name] AS [ref_pm_dept_name] FROM [ft_project] A LEFT JOIN [ft_department] B ON A.[owner_dept_rowid] = B.[sys_rowid] LEFT JOIN [ft_employee] C ON B.[pm_rowid] = C.[sys_rowid] LEFT JOIN [ft_department] D ON C.[dept_rowid] = D.[sys_rowid] ``` --- ### 🧩 案例五:多條件篩選(FilterGroup) ```csharp var filterGroup = FilterGroup.All( FilterCondition.Contains("sys_name", "專案"), FilterCondition.Equal("ref_pm_name", "張三") ); var sortFields = new SortFieldCollection { new SortField("sys_id", SortDirection.Asc) }; var command = builder.BuildSelectCommand("Project", "sys_id,sys_name", filterGroup, sortFields); ``` > 📘 說明: > `FilterGroup` 包含多個條件:其中一個是主檔欄位 (`sys_name`),另一個為參考欄位 (`ref_pm_name`)。 > 系統只會針對參考欄位自動建立 JOIN,避免不必要的多餘關聯。 **產生 SQL 範例:** ```sql SELECT A.[sys_id], A.[sys_name] FROM [ft_project] A LEFT JOIN [ft_employee] B ON A.[pm_rowid] = B.[sys_rowid] WHERE (A.[sys_name] LIKE @p0 AND B.[sys_name] = @p1) ORDER BY A.[sys_id] ASC ``` --- > 🧠 **重點說明:** > DR-Map 的 SQL 組成完全取決於查詢需求: > - **僅主檔欄位** → 無 JOIN > - **使用參考欄位(Select/Where/Order)** → 自動加入必要 JOIN > - **多參考欄位** → 多層關聯自動解析 > > 這讓開發者僅需描述「要什麼資料」,而不需思考「怎麼 JOIN」,實現真正的 **Definition-Driven Query** 模式。 ## 6️⃣ 為何 ERP 不適合傳統 ORM ERP 系統的資料結構與邏輯受 **使用者操作、參數、客製設定** 高度影響。若仍以強型別 ORM 綁定,容易造成「型別爆炸」與「效能瓶頸」。 | 特性 | 說明 | |------|------| | **動態欄位** | 欄位會依角色、權限、公司別或版本動態出現/隱藏。 | | **客製化需求** | 不同客戶可自訂欄位、邏輯與檢核條件。 | | **多樣回傳結構** | 同一表單的查詢結果,可能依參數產生不同欄位集。 | | **動態關聯來源** | 例如報價單可依業務情境關聯不同資料來源(Customer/Lead/Partner)。 | 在這樣的情況下,若仍使用 ORM 強型別類別: - 每新增一種來源關聯,ORM 類別就需重編。 - 類別數量膨脹、維護成本高。 - 執行效能受限於固定 JOIN 結構。 --- ## ✅ 結語 BeeNET 的 **DR-Map(Definition Relational Mapping)** 不僅是 ORM 的替代方案,更是企業級系統動態資料架構的核心設計思想。DR-Map 是 BeeNET 資料層(Bee.Db)與定義層(Bee.Define)協作的關鍵實作,使整體架構由靜態類別轉為動態定義驅動的系統核心。 - 🚀 **快速變更**:資料結構改動即時生效 - 🧠 **自動關聯**:跨表 JOIN 由定義檔決定 - ⚙️ **低耦合**:模組以定義檔連結,擴充容易 - 🧩 **可客製**:支援多租戶、多版本共用架構 > DR-Map 讓 ERP 系統真正落實「以定義驅動」的理念,實現不中斷服務、不重編程的即時調整能力,為企業帶來靈活、穩定且可持續演進的資料關聯架構。 --- **📢 歡迎轉載,請註明出處** **📬 歡迎追蹤我的技術筆記與實戰經驗分享** [Facebook](https://www.facebook.com/profile.php?id=61574839666569) | [HackMD](https://hackmd.io/@jeff377) | [GitHub](https://github.com/jeff377) | [NuGet](https://www.nuget.org/profiles/jeff377)