徐照興
(江西服裝學(xué)院 大數(shù)據(jù)學(xué)院,江西南昌向塘經(jīng)濟(jì)開發(fā)區(qū)麗湖中大道103號(hào) 330201)
決定一個(gè)系統(tǒng)優(yōu)劣最關(guān)鍵的是系統(tǒng)的架構(gòu),系統(tǒng)架構(gòu)的優(yōu)劣決定著系統(tǒng)具有的延遲與吞吐量、可用性與一致性、可擴(kuò)展性、穩(wěn)定性等[1]。系統(tǒng)經(jīng)典架構(gòu)主要有基于接口編程的三層架構(gòu),該架構(gòu)可以很好地從整體上對(duì)系統(tǒng)解耦。經(jīng)典三層通常分為數(shù)據(jù)訪問層、業(yè)務(wù)邏輯層、表現(xiàn)層,大型系統(tǒng)通常會(huì)在經(jīng)典三層架構(gòu)的基礎(chǔ)上對(duì)業(yè)務(wù)邏輯層進(jìn)行再封裝,使之形成新的一層,通常稱為服務(wù)層,這樣系統(tǒng)的架構(gòu)就變成了四層,也就成為分布式系統(tǒng)架構(gòu)[2]。不論幾層架構(gòu),數(shù)據(jù)訪問層是必須的,其功能主要是負(fù)責(zé)對(duì)數(shù)據(jù)庫的訪問。文中經(jīng)過對(duì)多套開源框架分析,結(jié)合實(shí)戰(zhàn)開發(fā)經(jīng)驗(yàn),給出一種基于接口編程思想,用Entity Framework技術(shù)實(shí)現(xiàn)數(shù)據(jù)訪問層的方案,并給出核心代碼,整個(gè)實(shí)現(xiàn)思路如圖1所示。
圖1 數(shù)據(jù)訪問層實(shí)現(xiàn)思路總圖Fig.1 General diagram of data access layer implementation idea
在.Net開發(fā)平臺(tái)下,數(shù)據(jù)訪問層實(shí)現(xiàn)技術(shù)主要有ADO.Net,NHibernate,Entity Framework(以下簡稱“EF”)。ADO.Net是一種最基本的數(shù)據(jù)庫訪問技術(shù),訪問性能高,需要比較熟練掌握SQL Server數(shù)據(jù)庫技術(shù)。EF是典型的一種實(shí)現(xiàn)了ORM(Object Relational Mapping,對(duì)象關(guān)系映射)框架的技術(shù),它底層是對(duì)ADO.Net技術(shù)進(jìn)行了封裝,通過實(shí)體、關(guān)系型數(shù)據(jù)庫表之間的映射,使開發(fā)人員可以通過操作表實(shí)體而間接地操作數(shù)據(jù)庫,大大地提高了開發(fā)效率。EF與原生ADO.NET技術(shù)相比缺點(diǎn)是訪問性能稍差,主要是因?yàn)樵诰幾g運(yùn)行時(shí)有一個(gè)生成sql腳本的過程[3]。NHibernate也是一種實(shí)現(xiàn)了ORM框架的技術(shù),它使用數(shù)據(jù)庫和配置信息來為應(yīng)用程序提供持久化服務(wù),也即在使用時(shí)要進(jìn)行更多的配置[4],總體來說與Entity Framework技術(shù)差不多,但是NHibernate與Visual Studio開發(fā)環(huán)境的集成不如EF,所以本文選擇主要利用EF技術(shù)來實(shí)現(xiàn)數(shù)據(jù)訪問層。
系統(tǒng)采用Model First方式設(shè)計(jì)數(shù)據(jù)庫,即先設(shè)計(jì)Model,然后根據(jù)Model生成數(shù)據(jù)庫。為了方便后面給出實(shí)現(xiàn)代碼及說明,假設(shè)系統(tǒng)有UserInfo(用戶)和OrderInfo(訂單)兩個(gè)實(shí)體,UserInfo實(shí)體擁有UName(用戶名)、Pwd(密碼)、ShowName(真實(shí)姓名)等屬性,OrderInfo實(shí)體擁有Content(訂單內(nèi)容)、UserInfoId(訂單所屬的用戶Id)等屬性,它們之間的關(guān)系為1對(duì)多,實(shí)體數(shù)據(jù)模型如圖2所示。
圖2 數(shù)據(jù)實(shí)體模型Fig.2 Data entity model
數(shù)據(jù)訪問層最基本職責(zé)就是封裝對(duì)實(shí)體的增刪改查方法,假設(shè)要對(duì)實(shí)體UserInfo進(jìn)行封裝,即創(chuàng)建UserInfoDal類,此類中用EF對(duì)實(shí)體進(jìn)行增加、修改、刪除方法都非常簡單。下面重點(diǎn)分析闡述如何根據(jù)用戶輸入的任意條件高效查詢數(shù)據(jù)。在ADO.Net數(shù)據(jù)庫訪問技術(shù)時(shí)往往會(huì)采用where拼接條件,這樣寫起來很繁瑣,性能比較差,也不利于擴(kuò)展。采用EF來實(shí)現(xiàn),要考慮參數(shù)類型和返回值類型2個(gè)問題。
(1)方法參數(shù)類型
要能接受用戶輸入的任意條件是一個(gè)條件,它要么為真要么為假,其返回值是bool類型,因此用委托Func
(2)方法返回值類型
返回值是用戶,但返回的用戶個(gè)數(shù)是不確定的,因此不能用UserInfo,可以用List
該方法參數(shù)Func
用Expression
利用EF技術(shù)實(shí)現(xiàn)根據(jù)用戶輸入任意條件高效查詢數(shù)據(jù)的代碼如下:
Public IQueryable
{
Return
db.UserInfo.Where(whereLambda).AsQueryable();
}
Lambda分頁查詢需要的參數(shù)有pageSize(每頁多少條記錄)、pageIndex(查找的頁碼索引)、查詢條件、排序條件、升序(asc)還是降序(desc),此外,一般還會(huì)輸出查找到的記錄數(shù)total。最關(guān)鍵的就是查詢條件參數(shù)及排序條件參數(shù)如何表示。
查詢條件與3.1節(jié)中的是一樣的,重點(diǎn)分析排序條件。
排序條件是由用戶傳入,因此跟查詢條件參數(shù)類似。但Func的返回值類型不是固定的bool,如果根據(jù)Id排序就是int,如果根據(jù)用戶名排序就是string等,具體實(shí)現(xiàn)如下:
把分頁查詢方法寫成泛型形式,也即泛型方法,這樣在調(diào)用方法時(shí)由用戶傳入,也即排序條件參數(shù)為:Expression
分頁查詢返回值類型同樣為延遲加載類型,因此返回值類型設(shè)置為IQueryable
為了提高代碼的復(fù)用性及系統(tǒng)的可擴(kuò)展性,需要封裝數(shù)據(jù)訪問層的基類,取名為BaseDal,其功能就是封裝所有Dal層類公共的增刪改查方法。然后Dal層各實(shí)體類(比如UserInfoDal,OrderInfoDal等)只要繼承基類BaseDal就擁有了增刪改查方法。那么在基類下面所有方法就不能指明具體的類型,那如何處理呢?
子類UserInfoDal繼承基類BaseDal時(shí),可以通過子類傳入類型給基類,因此,基類就要能接收子類傳入的類型,也即基類要采用泛型類,同時(shí)約定傳入的類型要為引用類型和具有無參構(gòu)造方法。
通常采用new實(shí)例化對(duì)象,會(huì)導(dǎo)致BLL層與DAL層緊密耦合在一起,即DAL層發(fā)生變化BLL層就必須跟著變。而項(xiàng)目設(shè)計(jì)原則為模塊內(nèi)高內(nèi)聚、模塊間低耦合[6]。即當(dāng)DAL層發(fā)生變化,BLL層不需要變化或者變化達(dá)到最小。
可以通過接口隔離BLL層對(duì)DAL的依賴,即讓BLL層依賴接口,不要依賴于DAL層,因?yàn)镈AL層是具體的實(shí)現(xiàn)(即不依賴于具體實(shí)現(xiàn)),而依賴于接口,接口是抽象的(里面的方法等都只有一個(gè)定義而已),可以用不同的方法來實(shí)現(xiàn)接口,因此,需要建立接口層。然而有多少個(gè)實(shí)體,就需要建立多少個(gè)實(shí)體對(duì)應(yīng)的接口,且每個(gè)接口里都是定義類似的抽象的增刪改查方法,因此,可以抽象出基類接口IBaseDal,同BaseDal一樣,它也需要設(shè)置為泛型,以便接收子類傳入的類型。
有了上面的基類接口后,具體的子類接口(如IUserInfoDal)只需要去繼承基類接口IBaseDal,并傳入對(duì)應(yīng)的實(shí)體類型即可。
為了讓實(shí)例化對(duì)象返回值類型為接口類型,還需要讓對(duì)應(yīng)數(shù)據(jù)訪問層子類(如UserInfoDal)去實(shí)現(xiàn)對(duì)應(yīng)的接口。具體數(shù)據(jù)訪問層子類(UserInfoDal)的代碼結(jié)構(gòu)升級(jí)改為如下形式:
Public class UserInfoDal:BaseDal
{
}
然后,實(shí)例化具體數(shù)據(jù)訪問層子類就可以用接口類型去接收(即返回值類型為接口)。
3.5通過抽象工廠實(shí)現(xiàn)BLL層與DAL層徹底解耦
通過接口隔離BLL對(duì)DAL的依賴,代碼得到較大優(yōu)化,但是還有不足之處。
因?yàn)闃I(yè)務(wù)邏輯層有很多BLL類,比如UserInfoBll,OrderInfoBll等,有多少個(gè)實(shí)體類就需要有多少個(gè)BLL類,而且UserInfoDal會(huì)用得非常頻繁,因?yàn)楹芏鄻I(yè)務(wù)BLL都需要與用戶發(fā)生數(shù)據(jù)交互,如UserInfoDal()名稱發(fā)生改變(比如數(shù)據(jù)訪問驅(qū)動(dòng)層實(shí)現(xiàn)技術(shù)發(fā)生改變,名稱由UserInfoDal改為了NhUserInfoDal),那么所有用到UserInfoDal的BLL類都要做相應(yīng)的更改,這樣就非常麻煩,那么有沒有辦法改一個(gè)配置就可以呢?
這就可以用抽象工廠,其本質(zhì)是使用反射方式來實(shí)現(xiàn)[7]。要使用反射就要獲得當(dāng)前程序集。數(shù)據(jù)訪問驅(qū)動(dòng)層的不同實(shí)現(xiàn)方法其實(shí)就是程序集名稱不同,不過前提是不同數(shù)據(jù)訪問驅(qū)動(dòng)層封裝同一個(gè)實(shí)體的DAL名稱要相同,這實(shí)際也就是約定大于配置思想。在項(xiàng)目中新建一個(gè)靜態(tài)的StaticDalFactory類,在該類下面創(chuàng)建GetUserInfoDal方法,返回值類型設(shè)置為接口類型IUserInfoDal。
由于變化點(diǎn)只有一個(gè)程序集名稱,因此,可以把程序集的名稱放到配置文件(web.config)中去。
對(duì)于靜態(tài)工廠層(StaticDalFactory),首先添加對(duì)配置文件的引用,即添加對(duì)System.Configuratuion程序集的引用。
那么以后實(shí)現(xiàn)數(shù)據(jù)訪問驅(qū)動(dòng)層的技術(shù)發(fā)生改變,只要修改配置文件web.Config中的
前面數(shù)據(jù)訪問層BaseDal中上下文實(shí)例[8]是通過new產(chǎn)生的,只要執(zhí)行到new所在代碼就會(huì)產(chǎn)生一個(gè)上下文實(shí)例??梢杂靡粋€(gè)單獨(dú)類來產(chǎn)生上下文實(shí)例,再新建一個(gè)DbContentFactory類,功能用來保證線程內(nèi)共享一個(gè)上下文實(shí)例。
在此類下創(chuàng)建一個(gè)方法GetCurrentDbContent(),通過該方法用來創(chuàng)建上下文對(duì)象,返回值類型設(shè)置為DbContext,因?yàn)镸odel層中如果還有其他的上下文對(duì)象,那么只要把new后面的具體上下文對(duì)象名稱更改就可以切換上下文對(duì)象。
在數(shù)據(jù)訪問層繼續(xù)封裝一個(gè)DbSession類,讓它擁有所有DAL的實(shí)例和更新到數(shù)據(jù)庫的方法,也即是讓DbSession類為整個(gè)數(shù)據(jù)訪問層與數(shù)據(jù)庫的會(huì)話類,像EF上下文封裝了所有表對(duì)應(yīng)實(shí)體集合DbSet
接下來把數(shù)據(jù)訪問層基類BaseDal.cs中所有Db.SaveChanges()代碼全部刪除,也即是不要在數(shù)據(jù)訪問層每一個(gè)操作都去與數(shù)據(jù)庫交互。這樣做優(yōu)點(diǎn)是數(shù)據(jù)提交的權(quán)利從數(shù)據(jù)庫訪問層提到了業(yè)務(wù)邏輯層。如果在數(shù)據(jù)庫訪問層每個(gè)方法都有SaveChanges()方法,那么每操作一次都會(huì)與數(shù)據(jù)庫發(fā)生交互。而到了業(yè)務(wù)層來了就非常靈活,多個(gè)操作可以“積攢”一起提交,當(dāng)然需要的話也可以一個(gè)操作結(jié)束了就提交,只要加上DbSession.SaveChanges()語句即可。
業(yè)務(wù)邏輯層中語句:DbSession dbsession=new DbSession();即DbSession所在層與BLL層之間緊密依賴,因此需定義一個(gè)IDbSession類,用來隔離DbSession所在層與BLL層之間的依賴,然后讓DbSession去實(shí)現(xiàn)IDbSession。
但是這里還有問題,就是通過new實(shí)例化對(duì)象,只要執(zhí)行,就會(huì)產(chǎn)生新的實(shí)例。因此需要封裝成一次請(qǐng)求共用一個(gè)實(shí)例(比如封裝為GetCurrentDbSession方法)。然后回到業(yè)務(wù)邏輯層改造獲取dbsession實(shí)例代碼,采用上面封裝好的方法,這樣系統(tǒng)就又進(jìn)一步得到了優(yōu)化。后續(xù)要得到userInfoDal等各個(gè)實(shí)例的DAL,可以通過統(tǒng)一的數(shù)據(jù)庫訪問入口DbSession來獲取。
至此,數(shù)據(jù)訪問層從可擴(kuò)展性、可復(fù)用性及可維護(hù)性等方面均得到了非常大的優(yōu)化。
系統(tǒng)具有可擴(kuò)展性、可復(fù)用性及可維護(hù)性是一個(gè)基本要求,文中基于接口編程的思想,選擇.Net平臺(tái),采用EF技術(shù),一步步分析闡述了如何對(duì)數(shù)據(jù)訪問層進(jìn)行優(yōu)化,并給出核心代碼,從而加深加速軟件技術(shù)開發(fā)人員更好地理解數(shù)據(jù)訪問層常用的技術(shù)思想,為同行提供一種數(shù)據(jù)訪問層的實(shí)現(xiàn)技術(shù)參考。數(shù)據(jù)訪問層實(shí)現(xiàn)技術(shù)不是一成不變的,沒有最好,只有更好,比如數(shù)據(jù)訪問層實(shí)例對(duì)象可以通過依賴注入方式(Spring.Net)注入到業(yè)務(wù)邏輯層,讀者可以在理解的基礎(chǔ)上根據(jù)系統(tǒng)的實(shí)際業(yè)務(wù)需求作出選擇或進(jìn)一步優(yōu)化。