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

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)