從最初投身 Java 開發,到後來專注於 .NET 平台,經手過大大小小的專案不計其數。職業生涯初期,每完成一個程式都能獲得滿滿的成就感;但隨著開發年資增長、代碼行數激增,寫代碼逐漸從興趣變成了謀生工具。面對海量雷同的 SQL 語句,尤其是維護舊系統時對 SQL 的調試,更是讓人頭痛不已。於是,我一直致力於尋找一種方法,能夠擺脫重複編寫繁瑣 SQL 的命運。
Java 平台上有成熟的 Hibernate ORM 框架,理應能解決這個問題。我迫不及待下載並用 MyEclipse 編寫測試程式,卻發現體驗遠不如預期。最大的阻礙在於那個 XML 配置文件:雖然借助 IDE 配置尚可接受,但一旦脫離 IDE,維護配置文件的困難程度絲毫不亞於直接維護 SQL 命令。當然,好處也是有的——至少不用逐條調試 SQL。雖然不太情願,但考慮到脫離 IDE 的情況較少,便「愛用不用」地用了一段時間。
隨後機緣巧合轉向 .NET 平台,從 .NET Framework 1.1 和 VS2003 起步,到 .NET Framework 2.0 和 VS2005,隨著開發經驗豐富、對 C# 特性理解深入,我終於決定親手打造一個更貼合 C# 風格的 ORM 框架:完全面向物件、更簡潔、更高效、對 IDE 智能感知支援更好(早期綁定)。
假設有一張員工表,欄位為 id、employee_id、name、department_id,對應實體類 EmployeeInfo。我們需要為這張表編寫資料存取類,提供增刪改查功能。以下是完整實現代碼:
public class EmployeeTable : TableAccess<EmployeeInfo> {
internal override ColumnParameter[] FillColumnParameters(EmployeeInfo entity) {
return base.BuildColumnParameter(
entity.ID,
entity.EmployeeID,
entity.Name,
entity.DepartmentID
);
}
public override string TableName {
get { return "employee_table"; }
}
public override TableColumnCollection TableColumns {
get {
TableColumnCollection list = new TableColumnCollection(4);
list.AddRange(new TableColumn[] {
new TableColumn("id", true), // 自增唯讀欄位
new TableColumn("employee_id"),
new TableColumn("name"),
new TableColumn("department_id")
});
return list;
}
}
internal override EmployeeInfo BuildEntity(MySqlDataReader reader, int startIndex) {
EmployeeInfo info = new EmployeeInfo();
info.ID = reader.GetInt32(0);
info.EmployeeID = reader.GetString(1);
info.Name = reader.GetString(2);
info.DepartmentID = DataReadyUtility.GetInt32(reader, 3);
return info;
}
}
從代碼可見,針對單表存取,只需重寫抽象基類的 4 個方法,即可獲得以下功能:
GetByPrimaryKey(object)GetAll()Search(SearchConditionCollection, ...)Search(SearchConditionCollection, int, int, ...)Save(EmployeeInfo)Modify(...)Modify(SearchConditionCollection, ...)Delete(...)Delete(SearchConditionCollection, ...)為了豐富功能,可對 EmployeeTable 進行擴充:
public class EmployeeTable : TableAccess<EmployeeInfo> {
// 修改員工姓名
public int ChangeName(int id, string name) {
EmployeeInfo info = new EmployeeInfo();
info.Name = name;
// 僅修改指定欄位 TableColumns["name"]
return base.Modify(id, info, ModifyBounds.None, TableColumns["name"]);
}
// 按姓名查詢(不分頁)
public IList<EmployeeInfo> GetByName(string name) {
SearchConditionCollection filter = new SearchConditionCollection();
filter.Add(new SearchCondition("name", new ColumnComparison(SqlOperator.Like, name)));
return base.Search(filter);
}
// 按姓名分頁查詢,輸出總筆數供前端分頁
public IList<EmployeeInfo> GetByName(string name, int pageSize, int pageIndex, out int itemCount) {
SearchConditionCollection filter = new SearchConditionCollection();
filter.Add(new SearchCondition("name", new ColumnComparison(SqlOperator.Like, name)));
return base.Search(filter, pageSize, pageIndex, out itemCount);
}
}
最後在業務邏輯層調用上述方法滿足專案需求:
public class EmployeeTableTest {
void Test() {
EmployeeTable employeeDAL = new EmployeeTable();
SearchConditionCollection filter = new SearchConditionCollection();
// 新增員工
EmployeeInfo employee = new EmployeeInfo();
employee.Name = "wfyfngu";
employee.DepartmentID = 4;
employeeDAL.Save(employee);
// 主鍵查詢
EmployeeInfo info = employeeDAL.GetByPrimaryKey(1);
// 查詢所有
IList<EmployeeInfo> all = employeeDAL.GetAll();
// 分頁查詢
int itemCount;
IList<EmployeeInfo> pagedSource = employeeDAL.Search(null, 10, 1, out itemCount);
// 修改姓名
employeeDAL.ChangeName(1, "wfyfngu");
// 刪除多筆(僅示範,實際應避免硬編碼欄位名)
filter.Add(new SearchCondition("user_id", new ColumnComparison(SqlOperator.In, 1, 2, 5, 7)));
employeeDAL.Delete(filter);
// 模糊查詢 + 分頁
filter.Clear();
filter.Add(new SearchCondition("name", new ColumnComparison(SqlOperator.StartWith, "wfy")));
IList<EmployeeInfo> result = employeeDAL.Search(filter, 10, 1, out itemCount);
}
}
框架還支援簡單的聯表查詢、視圖查詢等功能。附件提供了針對 MySQL 編寫的 ORM 替代框架原始碼(因快取功能涉及另一類庫,附件中已移除,需自行加入)。主要類別包括:
ReadonlyTableAccess<T>TableAccess<T>JoinTableAccess<T>這是一個輕量、高效、貼合 C# 開發習慣的 ORM 替代方案,歡迎有興趣的開發者參考學習。