iT邦幫忙

0

從 ORM 到 DR-Map:ERP 系統資料關聯的進化之路

  • 分享至 

  • xImage
  •  

dr-map

ERP 系統往往面對高度變動的流程與資料結構。傳統以強型別類別綁定資料表的 ORM(Object Relational Mapping)模式,對於「版本快速迭代」與「客製化需求」顯得僵化。BeeNET 採用 DR-Map(Definition Relational Mapping) 模型,以「定義(Definition)」為核心,在執行階段動態建立資料關聯,形成 Definition-Driven Architecture(定義驅動架構),可在不重新編譯程式的情況下即時調整欄位與關聯。


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 指令,實現靈活的資料查詢與關聯運作。

https://ithelp.ithome.com.tw/upload/images/20251107/200079568K3NcwknTu.jpg


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 範例連結:

<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。

/// <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,涵蓋從單純查詢到多重參考的完整範圍。


🧩 案例一:僅查詢主檔欄位(無關聯)

var builder = new SqlFormCommandBuilder("Project");
var command = builder.BuildSelectCommand("Project", "sys_id,sys_name");

📘 說明:
僅選取 Project 主檔欄位,不包含任何參考欄位,因此不產生 JOIN。

產生 SQL 範例:

SELECT
    A.[sys_id],
    A.[sys_name]
FROM [ft_project] A

🧩 案例二:依參考欄位過濾(Where 條件)

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 範例:

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 條件)

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 範例:

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

🧩 案例四:同時選取多個參考欄位

var command = builder.BuildSelectCommand("Project", "sys_id,sys_name,ref_owner_dept_name,ref_pm_dept_name");

📘 說明:
若選取欄位包含多個參考欄位(例如專案負責人部門與專案經理部門),DR-Map 會依據各欄位的 RelationProgId 自動建立多層 JOIN。

產生 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)

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 範例:

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 系統真正落實「以定義驅動」的理念,實現不中斷服務、不重編程的即時調整能力,為企業帶來靈活、穩定且可持續演進的資料關聯架構。


📘 HackMD 原文筆記:
👉 https://hackmd.io/@jeff377/drmap

📢 歡迎轉載,請註明出處
📬 歡迎追蹤我的技術筆記與實戰經驗分享
FacebookHackMDGitHubNuGet


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言