本篇將實際展示 GIX4 系統如何應用聚合 SQL 方案改善效能,並與既有架構整合。內容涵蓋 SQL 生成、資料映射讀取方案、工廠方法命名慣例與完整實例程式碼。
GIX4 採用 CSLA 作為分散式框架,ORM 使用輕量級開源專案 LiteORM。領域模型類別定義包含資料表與欄位的中繼資料(Attribute 形式),為 SQL 自動生成奠定基礎。雖然高效能模組較少,核心查詢 SQL 仍以人工撰寫為主,但列名生成必須自動化以避免多表聯結時的欄位名稱衝突,並支援結構變更不影響手寫 SQL。
我們在基類 GEntity<T> 中封裝列名生成 API:
[Serializable]
public abstract class GEntity<T> : Entity<T> where T : GEntity<T>
{
///
/// 從 DataRow 直接讀取資料。欄位約定為 "表名_欄位名",如 "PBS_Name"。
///
public static T ReadDataDirectly(DataRow rowData) { ... }
///
/// 獲取可用於 ReadDirectly 讀取的列名 SQL,格式:PBS.Id as PBS_Id, PBS.Name as PBS_Name...
///
public static string GetReadableColumnsSql() { ... }
///
/// 支援表別名的列名 SQL 生成。
///
public static string GetReadableColumnsSql(string tableAlias) { ... }
public static string GetReadableColumnSql(string columnName) { ... }
public static ITable GetTableInfo() { ... }
}
private static readonly string SQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES =
string.Format(@"
select
{0}, {1}, {2}
from PBS pbs
left outer join PBSProperty p on pbs.Id = p.PBSId
left outer join PBSPropertyOptionalValue v on p.Id = v.PBSPropertyId
where pbs.PBSTypeId = '{{0}}'
order by pbs.Id, p.Id",
PBS.GetReadableColumnsSql(),
PBSProperty.GetReadableColumnsSql("p"),
PBSPropertyOptionalValue.GetReadableColumnsSql("v"));
生成的 SQL 列名格式統一為 表名_欄位名(或 別名_欄位名),儲存於靜態唯讀欄位,避免重複生成開銷。
聚合查詢返回的扁平化資料表結構如下:
TableA TableB TableC TableD... a1 b1 c1 d1 a1 b1 c2 NULL a1 b2 c3 NULL a1 b3 NULL NULL a2 b4 c5 NULL ...
對應的物件層級結構為樹狀:每個 TableA 行對應一個子表,子表再對應更細粒度的子表。我們定義 IGTable 介面與兩個實作類別:
public interface IGTable
{
int Count { get; }
DataRow this[int rowIndex] { get; }
}
public class GTable : IGTable { /* 適配 DataTable */ }
public class SubTable : IGTable
{
/* 表示父表中指定範圍的行集合,構造函數傳入父 IGTable、起始行、結束行 */
}
核心讀取邏輯封裝於 EntityListHelper.ReadFromTable:
public static void ReadFromTable<TCollection, TEntity>(
TCollection list, IGTable table, Func<TEntity, IGTable, TEntity> relationLoader)
where TCollection : GBusinessListBase<TCollection, TEntity>
where TEntity : GEntity<TEntity>
{
// 演算法:遍歷 table,依據 TEntity.Id 變化切分子表,
// 為每個 TEntity 建立 SubTable 傳給 relationLoader 遞迴載入子物件
...
}
relationLoader 由呼叫端定義,負責將子表資料映射為導航屬性集合,實現靈活的聚合物件構建。
為保持資料存取層介面語意清晰,不暴露 ORM 細節,我們採用命名慣例而非表達式樹:
GetArticles_With_UserGetPBSTypes_With_PBSTree註解說明該方法查詢出的物件所攜帶的關聯物件。此設計簡單實用,適合後期以 SQL/預存程序優化替換 ORM 的場景。
// 客戶端呼叫入口
public static PBSs GetListByPBSTypeId_With_Properties(Guid pbsTypeId)
{
return DataPortal.Fetch<PBSs>(new GetListCriteria_With_Properties(pbsTypeId));
}
// 服務端 DataPortal_Fetch
private void DataPortal_Fetch(GetListCriteria_With_Properties criteria)
{
var sql = string.Format(SQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES, criteria.PBSTypeId);
using (var db = Helper.CreateDb())
{
IGTable table = db.QueryTable(sql);
this.ReadFromTable(table, PBS.GetChild_With_Properties);
}
}
// PBS 類別的 relationLoader
public class PBS : GTreeEntity<PBS>, IDisplayModeL
{
public static PBS GetChild_With_Properties(PBS pbs, IGTable subTable)
{
pbs = GetChild(pbs);
var properties = PBSPropertys.GetChild_WithValues(pbs, subTable);
pbs.LoadProperty(PBSPropertysProperty, properties);
return pbs;
}
}
// PBSPropertys 類別的 relationLoader
public class PBSPropertys : GEntityTreeList<PBSPropertys, PBSProperty>
{
public static PBSPropertys GetChild_WithValues(PBS pbs, IGTable table)
{
var properties = GetChild();
properties.ReadFromTable(table, PBSProperty.GetChild_With_Values);
foreach (var p in properties) p.PBS = pbs;
return GetChild().AddRange(properties.OrderBy(p => p.OrderNo));
}
}
// PBSProperty 類別的 relationLoader
public class PBSProperty : GEntity<PBSProperty>, ITreeNode, IOrderedObject
{
public static PBSProperty GetChild_With_Values(PBSProperty property, IGTable table)
{
var model = GetChild(property);
var values = PBSPropertyOptionalValues.GetChild(model, table);
model.LoadProperty(PBSPropertyOptionalValuesProperty, values);
return model;
}
}
// 葉子節點 PBSPropertyOptionalValues
public partial class PBSPropertyOptionalValues : GEntityList<PBSPropertyOptionalValues, PBSPropertyOptionalValue>
{
internal static PBSPropertyOptionalValues GetChild(PBSProperty parent, IGTable table)
{
var values = GetChild();
values.ReadFromTable(table);
foreach (var v in values) v.PBSProperty = parent;
return values;
}
}

var pbsList = PBSs.GetListByPBSTypeId_With_Properties(id);
foreach (var pbs in pbsList)
{
foreach (var property in pbs.PBSPropertys)
{
foreach (var optionalValue in property.PBSPropertyOptionalValues)
{
// 直接使用,無額外資料庫往返
}
}
}
雖然客戶端使用多層迴圈,但實際僅發生一次遠端資料存取。
聚合 SQL 模式常用於專案重構/優化階段:初期開發時無法預知上層需求,僅當業務流程完成分析後,才能針對性編寫聚合 SQL。本文展示的完整方案——從 SQL 生成、列名約定、階層式資料映射到工廠方法命名——在 GIX4 專案中有效解決了 N+1 查詢問題,大幅降低資料庫往返次數。下一篇將介紹預載入技術與 .NET 4.0 平行運算庫在系統中的應用。