Step by Step-構建自己的ORM系列-數據訪問層
一、開篇
距離上篇《Step by Step-構建自己的ORM系列-開篇》的時間間隔的太久了,很對不住大家啊,主要是因為有幾個系列必須提前先寫完,才能繼續這個系列,當然我也在
寫這幾個系列的過程中,對ORM這個系列中的原來的實現的想法有了新的認識和改進,當然這些都不是說是很先進的思想或者認識,也可能是大家見過的思路吧,希望后面我能在
寫設計模式系列的過程中,穿插講解ORM系列,當然我的這個構建的系列,也只能說是很簡易的,自己平時開發個小應用工具或者什么的,可能用他,因為是自己開發的嘛,畢竟
使用起來還是比較順手的!符合自己的操作習慣嘛。
當然我寫這個系列的過程中,也會有自己認識偏激的地方,或者思路不正確的地方,還請大伙多多指出和批評。我也是在我目前的項目中學習到了很多的寶貴的經驗,其實
我們應該能看到ORM給我們提供的方便和不便之處,我們取其精華,剔除糟粕,不過這真的很難。我其實對一些流行的ORM的底層實現,研究的不多也不深,像Nhibernate,我
只是了解Hibernate,是當時從JAVA中了解過來的,不深入,Castle框架倒是用過一段時間,EntityFreamWork,我也沒有用過,只是象征性的下載最新版本,體驗了下AOP的
方式,我感覺其實有很多的時候,我們使用AOP的方式,能夠改進我們程序的靈活性。這塊可能還需要大牛們多多指點。
我理想的ORM是實現持久化的透明,這個怎么理解呢?就是說我在程序的開發中,我不想在業務代碼中書寫相應的持久化操作,也不關心業務層中的去如何調用你的
ORM,去完成CRUD的操作。我只關心我的業務邏輯,這個有點像DDD(領域驅動開發)里面的領域層了,只關心領域內部的業務邏輯,而不關心其他的東西,這樣方便我們快速
的抓住關注的東西,而盡量讓與領域無關的東西不要影響業務領域邏輯的實現。
二、摘要
本篇主要開始講述《Step by Step-構建自己的ORM系列-數據訪問層》關于數據訪問層的部分,其實前面也對這塊的內容有了一定的介紹了,其實本篇就是教你如何完成
ORM中的數據訪問層的操作,這里是提供統一的數據訪問方法的實現。當然這里的操作還是主要集中在數據庫的操作,包括如何根據實體對象返回實體的列表,包括生成SQL語
句的幾類實現,還包括一些實現后續的ORM的配置管理的維護,這里就是提供可視化的XML文件的配置,這個具體怎么來做呢?因為我們平時針對ORM的使用,都是直接修改
XML文件,我們可以提供一個可視化的界面,讓那個用戶配置這些相應的設置。通過這些配置,我們可以實現數據庫的平滑的遷移,緩存應用的配置,包括其他的一些相關設置信
息。總體來說這些操作都可以依托于,我們這里的數據訪問層來完成。
我們來看看ORM中數據訪問層的重要作用和地位吧:
上圖我們知道,我們所有的相關功能的基礎,都是基于數據訪問層來做的,所以我們處理好這個層的相關邏輯
后,后續的問題就會比較容易開展。下面我們就會針對這些疑問開始
一個個的解決我們的數據訪問層應該提供的相關功能!大體的功能應該有如下功能:
1、持久化的操作方法CUD。可以擴展提供創建表+其他的修改表等相關的自動腳本工具。提供持久化透明的方式。
2、提供緩存服務,將對象的相應映射信息緩存起來,這樣后續執行生成語句等操作,效率上會是很大的提升。
3、我們在處理對象對于Update語句,應該能處理好只能更變化的信息,如果沒有發生變化,那么我們是不是不用執行更新操作了呢?減少數據庫的操作次數。
4、提供基礎的查詢方法,以后所有的基于ORM上的查詢基于這個查詢進行擴展。提供持久化透明的查詢方式。
5、并發和事務的控制。我們這里可能提供一個內部的版本號的方式來做,一旦修改過這個對象或者發生改變,任何時候的操作,我們都是針對這個版本號的記錄來做的,版本號
通過內部提供的方法來進行。
三、本文大綱
1、開篇。
2、摘要。
3、本文大綱。
4、ORM之數據訪問層分析。
5、ORM相關代碼實現。
6、本章總結。
7、系列進度。
8、下篇預告。
四、ORM之數據訪問層分析
我們先來針對上面的幾個問題,我們給出實現思路,來分析下給出的思路的可行性和如何實現的解析。具體的代碼下節給出核心實現方案。
4.1、提供通用的持久化的操作
這個具體的解析在上篇中已經給出了相應的思路了,我們通過在底層提供相應的方法來做。一般來說,對應數據庫的四種操作,我們在數據訪問層,也提供了相應的語句的
自動構造的過程,具體的構造,我們前面給出的實現方案是通過特性來實現,特性中定義具體的數據庫字段,類型,長度等一些列的參數。我這里就不復述了,我這里分析下我們
這樣實現的好處。我們知道繼承的方式是挺好的,我為什么這么說,通過提供一個基類,基類中定義通用的CUD的操作方法,這樣只要是繼承這個類的子類,都會有CUD的操作
方法了,但是我們為了提供持久化透明的方案,那么無疑,對于持久化的操作,我們就不希望由業務邏輯層中的業務邏輯對象來完成,那么如何來做呢?我們通過數據訪問層,提
供統一的操作方法,讓服務層來完成業務對象的持久化操作。這樣就能實現,持久化透明的方案。
所以我們可以這樣來做,在數據訪問層中,我們提供一個接口,接口中定義持久化操作的幾類方案,通過不同的實現配置,我們可以在XML配置文件中進行指定,我們采用
重量級的ORM還是輕量級的ORM,這樣我們也理想的實現了低耦合的特性。
同時,對于不同的文件的操作,我們可以支持多文件類型的寫入,當然對于不同的數據庫存儲,如果我們利用關系型數據庫我們需要ORM,對于對象數據庫的操作,或者
XML文件的操作,我們這時候的ORM就變了。
4.2、提供緩存服務
我們知道,我們第一篇中主要是通過自定義特性+反射的形式來處理:我們并沒有提供完整的特性操作,其實還有很多的情況,比如說,我們還可以提供對視圖的映射,提
供一個視圖的特性等。還有其他的特性有很多,后續會給出完整的代碼結構,我們知道自定義特性+反射,如果每次在將對象映射成數據庫表的時候,那么效率上是多么的低下
啊,那么這個時候,我們可以考慮使用緩存的方式,將數據庫表中的數據庫列與對象中的屬性列進行映射,我們可以把這些對應關系放在緩存中,那么如果我們在后續的處理中遇
到與數據庫相關操作,需要進行對象映射的時候,我們都先會去緩存中查找有沒有指定鍵值的映射數據庫表列存在,存在取出生成SQL語句,否則通過反射取出對應的數據庫表
列,放在緩存中。下面給出示意圖。將這個過程進行描述:
當然我這里給出的肯定是反向的根據對象生成操作數據的SQL語句的方式。這里沒有考慮,當數
據庫表發生變化的時候,我應該自動同步緩存中的映射集合信息,當然我們可以通過一定的策略,來實現這樣的雙向同步問題。例如如下的方式可能就是可行的方案。
通過上述的方式,我們通過同步組件,在每次進行數據操作之前,我們可以應用更好的策略,比如記錄或者遍歷文件的修改狀態,對比文件的最后修改日期,是不是發生修改,或者當某個文件發生修
改之后,我們記錄在某個配置文件中,這樣我們可以提高同步的效率,因為通過這樣的方式,我們不需要每次檢查對象是不是發生變化了,這樣我們如果發現對象沒有發生變化,
那么我們就不要讓同步組件去檢測對象是否發生變化,這樣就能提高效率,同時支持當映射對象發生變化的時候,我們不用修改我們的關系數據庫。大家都知道,面向對象設計建
模與關系數據庫的最大難題就是雙方的變化的同步性的方案是很難定的,我這里只是給出一個簡單的思路和方式,可能還有更好的方案,也請大家多多告訴我。
4.3、Update語句的操作
我不知道,你們在面試的時候,如果你經常開發底層的面向對象的ORM的時候,應該會遇到這樣的問題,我們在查詢一個映射對象的時候,我們可能只需要取出這個對象的
部分列,而不是全部的數據列,這個時候,我們如何指定呢?就是填充對象的時候,我們只需要填充指定的列?或者我們需要在在保存編輯的時候,我們不更新未發生變化的數據
庫,其實主要是要求,我們在生成SQL語句的時候,我們希望我們的更新語句中不要出現,沒有發生變化的數據列的設置,這個如何做到呢?我想我們可以通過如下的2種方式來
做。
1、通過對象的序列化,來復制一個對象,并且系統中緩存這個對象,在編輯之前,緩存,等到提交后,釋放這個對象。這個由系統默認的提供方法。前提是標記對象是編
輯狀態,這樣在修改對象之前進行復制,不然如果修改完了,再復制就沒有什么意義了
2、通過字典來保存更新數值的數據列,通過數據字典來存放。我們在字典中存放映射的數據列,將發生變化的數據列和數據列的值進行標記發生改變,我們在生成更新語
句的時候。直接遍歷這個集合,將列狀態發生改變的列生成相應的操作語句即可。這些都是可行的方式,我們在前面的架構設計中也提到過的。都是給過思路的,這里我也不多復
述了。
3、…。可能還有其他的更好的方式,還請大家多提出好的思路,我備注在這個位置!
4.4、提供基礎的查詢方法。
這里說的提供基礎的查詢方法,指的是基于數據庫操作之上,我們提供幾個常用的查詢方法,包括自動生成版本號的方法等,我們的版本號可以通過日期+流水號的形式來
生成,或者是其他的情況。GUID也是可行的辦法。不過維護起來可能不是很方便。所以底層提供相應的操作方法更容易來做。
我們這里考慮提供如下的基礎查詢方法,復雜的查詢方法,我們可以提供一個入口來做。例如我們提供如下幾類方法:
1、底層生成版本號的方法,自增ID流水號,根據不同的生成規則自定義設置ID生成規則,來組織生成ID的通用方法。
2、提供實體與數據庫行集之間的轉換,我們需要將數據庫記錄轉換為實體集合。通過查詢方法返回對象集合。這里提供返回指定主鍵的對象集合。
3、返回一個數據表或者視圖中的所有記錄。
4、返回傳入分頁個數,和分頁排序字段,分頁條件的分頁集合。
5、返回指定列的查詢方法。(這里沒有想到好的辦法,怎么樣的形式比較靈活能夠動態的指定返回的列,比如說1列,10列,5列等),希望大家提出好的意見和建議!
6、提供統一的入口,編寫SQL語句傳入到數據訪問層中進行查詢和檢索,根據指定的返回類型來返回泛型對象。
4.5、并發和事務控制
我想一個系統中必須考慮的就是事務處理了,我們進行批量操作的時候,如果數據不同步,那就太痛苦了,也是不能使用的系統的,我們希望我們的ORM能夠自動的集成
事務和并發,當然這里說的并發是當用戶數上升到一定量的時候,就會產生這樣的問題,理論上來說只要有2個以上的用戶,就必須考慮并發操作!并發我們有幾個控制的思路,
總體來說應該說說我們前面的設計的內部的一個自動生成的版本號,是最好的選擇。具體怎么個意思呢?我們來解釋下:
對于并發控制,我們知道,并發控制的問題:寫丟失,讀出來的數據是臟數據,無疑就是這么2個比較常見的問題,那么我們如何來對寫丟失進行限制呢?目前通用的方案
都是通過樂觀鎖來處理,二個人可以同時對某個信息進行讀取,但是只能有一個人在進行編輯,但是最后修改的內容會把前面修改的信息覆蓋掉,這是樂觀鎖的處理方式。
悲觀鎖,則是只要有人在修改,那么可能你不能進行修改,也不能讀取,這種方式,當然可以保證信息的修改的同步性和一致性,但是用戶的易用性和友好性方面不夠人性
化,相比來說,有人修改,就不能被其他人修改,但是可以讀取的方式體驗方面要差一些,不過各有使用的場景,一般來說,悲觀鎖是樂觀鎖的一個補充。
我們這里既不是樂觀鎖,也不是悲觀鎖的形式,通過版本來對某個記錄的版本進行記錄,一旦發生改變,那么記錄的版本就要發生變化,我們這里對這個行集的版本的更新
可以通過ORM提供的版本的生成規則來生成一個版本號,或者是通過觸發器來實現,當然性能也是我們需要考慮的部分。
對于事務,我想一般的不是分布式操作的應用,我們通過數據庫提供的本身的事務服務來完成,基本上就可以滿足日常的需求,也沒有什么特別難的地方,我想這里我也就
不詳細的說了,我們來簡單的說下,分布式事務的一致性,對于這種分布式的事務操作,我們可以采用離線并發模式來處理。這個怎么理解呢?就是通過工作單元來實現。我們把
每一個操作看著一個工作單元,如果我們在執行某個事務操作的過程中,如果返回是0或者是其他的不是我們期望的結果時,我們不會進行任何的提交操作,如果全部執行通過,
我們循環所有的工作單元進行提交,否則我們回滾所有的系統事務。我們把這樣的分布式事務,看作一個業務事務,由一些列的工作單元組成,這些工作單元看作是系統事務。
五、ORM相關代碼實現
5.1、CUD的基本實現代碼:
1,Create語句的實現:
private Dictionary<string, Column> _autoIncrementColumns = new Dictionary<string, Column>();
private Dictionary<string, Column> _updateColumns = new Dictionary<string, Column>();public string TableName
{
get
{
return string.Empty;
}
}public Dictionary<string, Column> UpdateColumns
{
get
{
return this._updateColumns;
}
set
{
this._updateColumns = value;
}
}public Dictionary<string, Column> AutoIncrementColumns
{
get
{
return this._autoIncrementColumns;
}
set
{
this._autoIncrementColumns = value;
}
}public virtual IDbCommand GetDbCommand()
{
// 如果column的值沒有被更新過,則返回null
if (this.UpdateColumns.Count == 0)
{
return null;
}ArrayList fieldList = new ArrayList();
ArrayList valueList = new ArrayList();
SqlCommand cmd = new SqlCommand();foreach (Column column in this.UpdateColumns.Values)
{
fieldList.Add("[" + column.Key + "]");
valueList.Add("@" + column.Value);
}string fieldString = string.Join(" , ", (string[])fieldList.ToArray(typeof(string)));
string valueString = string.Join(" , ", (string[])valueList.ToArray(typeof(string)));
string cmdText = string.Format("INSERT INTO [{0}]({1}) VALUES({2})",
this.TableName,
fieldString,
valueString);string sqlGetIndentityID = null;
if (this.AutoIncrementColumns.Count == 1)
{
sqlGetIndentityID = string.Format("SELECT [{0}] = SCOPE_IDENTITY()");
}if (sqlGetIndentityID != null)
{
cmdText = cmdText + " ; " + sqlGetIndentityID;
}cmd.CommandText = cmdText;
return cmd;
}
}下面給出Update語句,是從上面的更新集合中編輯,將列的狀態發生改變的列添加生成到語句中-示例代碼如下:
public virtual IDbCommand GetDbCommand()
{
// 如果column的值沒有被更新過,則返回null
if (this.UpdateColumns.Count == 0)
{
return null;
}ArrayList fieldList = new ArrayList();
ArrayList valueList = new ArrayList();
SqlCommand cmd = new SqlCommand();string updateSQL=string.Empty;
foreach (Column column in this.UpdateColumns.Values)
{
if (column.State)
updateSQL += "[" + column.Key + "]=" + "@" + column.Value;
}string cmdText= string.Format("UPDATE {0} SET {1}={2}", updateSQL);
cmd.CommandText = cmdText;
return cmd;
}至于刪除的代碼比較簡單,我這里就不給出刪除的代碼了,總體來說形式是相同的。
5.2、緩存服務代碼
我有2篇關于緩存的介紹,緩存中最難搞的問題就是緩存的過期的問題,對應反射的性能問題也是存在過期的問題,比如說我們的數據庫表發生變化,或者對象中的屬性發
生變化后,那么我們的緩存中的內容也需要進行更新,不然我們生成的數據庫操作語句將會不正確。我們這里的策略就是將映射出來的對象,放在服務器中的緩存中,當然對于
B/S和C/S系統中可能采取的緩存方式和策略還是有區別的。B/S我們的緩存可以采用緩存到服務器中,或者是通過緩存服務器來完成,一般是通過Remoting來將服務器與緩存
服務器完成通信。我們看看簡單的示例代碼吧:
public class Cache
{
private static System.Web.Caching.Cache cache = HttpRuntime.Cache;//這里是默認取當前應用程序的服務緩存。public static object Get(string key)
{
return cache[key];
}public static bool Remove(string key)
{
return !(null == cache.Remove(key));
}public static void Set(string key, object value)
{
cache.Insert(key, value, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(3));
}
}
上面給出的緩存類的示例代碼,具體的操作,使用反射后,將反射后對象元數據信息緩存起來,通過對象名來緩存:
具體代碼如下:
PropertyInfo[] property = null;
if (Cache.Get("") != null)
{
property = (PropertyInfo[])Cache.Get("");
}
else
{
Type t = Type.GetType("");property = t.GetProperties();
}通過上面的幾行簡單的代碼就能表達出我們上面講述的思路,具體如何過期,這個上面也給出了一些思路,可能大伙有更好的思路,我這里就不班門弄斧了。
5.3,提供基礎的查詢服務
我想大伙對于查詢語句的操作,應該說是司空見慣了吧,我們如何能更好的完成統一的查詢服務可能是我們關心的問題,我這里不會給出多數據庫的實現,但是可以給大伙
一個思路,我們這里定義返回的查詢命令的時候,如果說支持多數據的話,可以定義一個統一的接口,不同的數據庫提供不同的實現接口,然后根據統一的ORM配置來調用不同的
組件來生成SQL語句,完成調用操作。
相關的查詢服務代碼如下:
/// <summary>
/// 系統自動生成的版本號
/// </summary>
/// <returns></returns>
public string GetVersion()
{
return DateTime.Now.Year.ToString() + DateTime.Now.Month.ToString() + DateTime.Now.Day.ToString() + DateTime.Now.Minute.ToString() + DateTime.Now.Second.ToString();
}public int GetMax<T>()
{
//根據T的類型獲取T的最大列,完成查詢操作。
string sqlText = " Select MAX(ISNULL(列名,0))+1 FROM TableName";return 0;
}public List<T> GetAll<T>()
{
//根據T的類型獲取T的最大列,完成查詢操作。
string sqlText = " Select * FROM TableName";return new List<T>();
}public List<T> GetList<T>(string condition,int pagesize,string orderField)
{
//根據T的類型獲取T的最大列,完成查詢操作。
string sqlText = " Select * FROM TableName where " + condition + " order by " + orderField;return new List<T>();
}
上面給出的不是全部的代碼,部分代碼還是大家自己去完成吧,我這里想的是,一些客戶比較復雜的自定義代碼通過一個接口傳入的形式,來完成基礎查詢服務的調用。
我們這里給出通用的接口定義:
CommandType CommandType
{
get;
set;
}string whereCondition
{
get;
set;
}string orderCondition
{
get;
set;
}string TableName
{
get;
set;
}Column[] ColumnList
{
get;
set;
}string SQL
{
get;
set;
}
給出默認幾類示例的實現:
public class BaseSQL : ISelect
{
public System.Data.CommandType CommandType
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}public string whereCondition
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}public string orderCondition
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}public string TableName
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}public Column[] ColumnList
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}public string SQL
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}具體調用的代碼如下:
public class SpecialSQL : BaseSQL
{
public void Test()
{
this.TableName = "";
this.SQL = " SELECT * FROM TEST ";
this.whereCondition = " ID=4 ";
this.orderCondition = " ORDER BY ID DESC ";
}
}當然這里的繼承的方式不是很推薦,可以采用抽象工廠的模式,來創建這個查詢對象,然后我們在調用這個查詢對象的地方,我們可以自定義這個SQL查詢對象,后臺的
ORM自動解析,完成自定義SQL語句的統一查詢服務入口。
當然如果您有更好的方案,可以提出來,非常感謝!
六、本章總結
本文主要是講述ORM中的數據訪問層,我這里由于一些特殊的原因,代碼給出的不是特別的詳細,一方面是由于之前的那部分的代碼丟了,現在一時難以還原,所以造成有
些代碼給出的不是特別完整的情況,請大家見諒,文章中的有些部分的內容,我在實現的過程中也是遇到了不少的問題,我現在的具體問題列出來,也請大家幫我解決一下,我的
疑問,我目前在架構設計的過程中遇到如下的問題:
1、我在實現服務層持久化透明服務的時候,我也想把查詢服務透明,意思就是業務對象與服務層只通過DTO來完成,業務對象的所有數據都通過服務層的訪問來完成。
2、如果現在有比較復雜的業務邏輯的操作語句的時候,我的這個SQL語句放在數據訪問層好呢?還是放在哪里?應該具體的職責劃分要明確。
3、我只是希望業務邏輯層處理業務數據,具體的業務數據怎么來的,我想讓業務邏輯只關心DTO。
4、對于這樣的服務層提供的統一查詢方式的話,我在表現層調用的時候,如何傳參,能夠很好的組織參數的傳遞和調用,傳統的方式不再目前的考慮當中。