老久不上來(lái)寫(xiě)技術(shù)類(lèi)的東西了,偶爾回歸一下吧。(其實(shí),這篇文章8個(gè)月前寫(xiě)了個(gè)大半,后來(lái)一直沒(méi)有時(shí)間去完善,再后來(lái)就因?yàn)楦鞣N原因給放下來(lái)了。)
非常抱歉,由于需要發(fā)表其他文章的緣故,我只能忍著不修正文中一小部分錯(cuò)誤,以及增加一些有助免于誤解的內(nèi)容。這里特別說(shuō)明一下,本文不是要討論緩存機(jī)制的好壞,更不是要討論如何緩存對(duì)象。而是說(shuō)DAL/BLL上面對(duì)DataContext的處理。另外一個(gè)需要注意的地方,是修改了一個(gè)錯(cuò)誤,原來(lái)大部分都寫(xiě)成IQueryable了,實(shí)際上應(yīng)該是除了最后一個(gè)之外,都是IEnumerable。原因是什么需要大家想一下。此外,也需要大家注意的事,我所提出來(lái)的緩存,并不是直接利用Linq2Sql的代碼來(lái)緩存,而是指是否便于緩存。這部分的內(nèi)容,會(huì)在文章后面補(bǔ)充說(shuō)明。
Linq to Sql 用的人也應(yīng)該有些吧,我在cnblogs上面看老趙寫(xiě)的那幾篇文章(請(qǐng)看08年9月左右的文章),感覺(jué)也很有深度,有不少啟發(fā)。因此我也打算寫(xiě)一點(diǎn)我自己的實(shí)踐經(jīng)驗(yàn),希望也能同樣給大家一些有用的啟發(fā)吧。
我首先想要問(wèn)一下大家,Linq to Sql有哪些很特別的地方?這個(gè)問(wèn)題的答案肯定五花八門(mén),我說(shuō)一下我看到的一些問(wèn)題吧。
首先,Linq to Sql的基礎(chǔ)之一是DataContext,而另外一個(gè)基礎(chǔ),則是通過(guò)映射產(chǎn)生的實(shí)體類(lèi),以及這些實(shí)體類(lèi)的Table<>對(duì)象。這個(gè)不是廢話嘛!我想很多人都應(yīng)該知道這個(gè)最基本的知識(shí),不過(guò)卻不見(jiàn)得有多少人真正注意到,或者認(rèn)真思考一下這里面的“機(jī)關(guān)”。不知道“機(jī)關(guān)”在哪里,那么就不可能寫(xiě)出合適的代碼。比如說(shuō),在某個(gè)頁(yè)面里面(N層結(jié)構(gòu)沒(méi)有給弄好的情況下),或者在某個(gè)業(yè)務(wù)邏輯里面(有N層結(jié)構(gòu)),你的Linq to Sql的代碼是否是長(zhǎng)這樣的?
“對(duì)啊,就是長(zhǎng)這樣的,有什么問(wèn)題嗎?”當(dāng)然有問(wèn)題啦,否則我也不寫(xiě)這個(gè)隨筆了。不知道大家有沒(méi)有想過(guò)這么一個(gè)問(wèn)題,什么叫做Context?Context就是上下文,上下文的意思就是,依賴(lài)于這個(gè)上下文的對(duì)象,必須存活在這個(gè)上下文里面。脫離了這個(gè)上下文,那些對(duì)象就會(huì)出現(xiàn)錯(cuò)誤。事實(shí)上也確實(shí)如此:在上面的例子里面,從ProductInfos中得到的q.ToList(),里面的每一個(gè)元素都依賴(lài)于MyDataContext。換句話說(shuō)MyDataContext如果被注銷(xiāo)了,q.ToList()生成的對(duì)象也就會(huì)“部分功能失效”。
“失效就失效好了,反正該做的工作已經(jīng)做完了,q.ToList()也已經(jīng)利用完了。”不錯(cuò),在上面的例子里面,不會(huì)發(fā)生什么錯(cuò)誤。不過(guò)這么寫(xiě)的話,會(huì)比較難使用的。為什么這么說(shuō)?我舉一個(gè)具體的例子:這個(gè)網(wǎng)站需要用戶(hù)登錄,而所有的業(yè)務(wù)邏輯幾乎都依賴(lài)于當(dāng)前用戶(hù)。如果說(shuō),我們使用上面的using模式,那么我估計(jì)你的代碼不外乎是如下兩種情況:
1、每一次需要當(dāng)前用戶(hù)的地方,你都需要從數(shù)據(jù)庫(kù)讀取;或者
2、你把當(dāng)前用戶(hù)保存為全局變量了,但是你發(fā)現(xiàn)currentUser.CompanyInfo因?yàn)樯舷挛囊呀?jīng)拋棄了,因此是無(wú)法使用的,業(yè)務(wù)層不得不每一次都重新從數(shù)據(jù)庫(kù)讀取該用戶(hù)所屬公司的數(shù)據(jù)。
這兩種形式如下所示:
如果你是第一種情況,那么很明顯,你會(huì)有大量重復(fù)的SQL調(diào)用。
如果是第二種情況,其實(shí)也不見(jiàn)得好到哪里去。因?yàn)椋?/p>
1、currentUser可能不需要經(jīng)常取,但相關(guān)的其它內(nèi)容,由于上下文各自獨(dú)立,你還是經(jīng)常在重復(fù)的獲取的;
2、有一個(gè)地方你無(wú)法繞過(guò)去——如果你要修改當(dāng)前用戶(hù)的屬性,而這個(gè)全局的當(dāng)前用戶(hù)不是當(dāng)前Context產(chǎn)生的,你還非得從當(dāng)前Context取出來(lái),然后再修改;或者如果你企圖通過(guò)currentUser.CompanyInfo來(lái)訪問(wèn)的話,也會(huì)報(bào)錯(cuò)。
3、因?yàn)閏urrentUser的上下文已經(jīng)被拋棄了,因此程序會(huì)很容易設(shè)計(jì)成傳入的不是一個(gè)UserInfo,而是一個(gè)int類(lèi)型的Id值,否則底層很容易一不小心就用到這個(gè)實(shí)際上功能不全的對(duì)象,然后就拋出異常了。但這樣做的后果是,獲取同一個(gè)類(lèi)型的實(shí)體對(duì)象,可能會(huì)有各種不同的重載形式,例如:
IEnumerable<TransactionInfo> GetTransactionsByUserId(int userId);
IEnumerable<TransactionInfo> GetTransactionsByCompanyId(int companyId);
IEnumerable<TransactionInfo> GetTransactionsByCompanyId(int companyId, EAccountName account);
IEnumerable<TransactionInfo> GetTransactionsByCompanyId(int companyId, EAccountName account, ETransactionType transactionType);
因?yàn)檫@種設(shè)計(jì)實(shí)施之后,有時(shí)很可能就會(huì)出現(xiàn)只有userId的情況,那么這個(gè)時(shí)候即使UserInfo對(duì)象中其實(shí)也存在CompanyId的值,也還是要重新獲取一遍UserInfo對(duì)象。(注意,后面提到的方案,并非不需要緩存了,而是因?yàn)樵谕粋€(gè)Context下,可以有效地利用實(shí)體對(duì)象,因此你可以將BLL層設(shè)計(jì)依賴(lài)實(shí)體對(duì)象,而不是id值,因此緩存整個(gè)實(shí)體對(duì)象會(huì)更加容易。)為了簡(jiǎn)化這一過(guò)程,就可能會(huì)產(chǎn)生不同的獲取形式。
這樣設(shè)計(jì)完整個(gè)系統(tǒng)之后一跑,看著好像沒(méi)什么,但真正上線卻發(fā)現(xiàn)有點(diǎn)慢。當(dāng)我們打開(kāi)Sql server的Profiler一看,會(huì)發(fā)現(xiàn)很簡(jiǎn)單的一個(gè)頁(yè)面的訪問(wèn),其數(shù)據(jù)庫(kù)訪問(wèn)會(huì)搞到幾十次甚至上百次,其中有很多Sql語(yǔ)句是完全重復(fù)的。
這個(gè)問(wèn)題怎么解決呢?有人會(huì)說(shuō),加個(gè)緩存機(jī)制吧。也許吧,但這種增加復(fù)雜度的設(shè)計(jì),我覺(jué)得還是不得已而為之的一種做法。(或者說(shuō),緩存很好,那是另外一種解決方案,但不解決這個(gè)問(wèn)題。)我認(rèn)為更好的解決辦法是,將上下文在當(dāng)前頁(yè)面中緩存起來(lái)。所謂的上下文,就是一種運(yùn)行環(huán)境,而一次頁(yè)面訪問(wèn),其環(huán)境應(yīng)該是相同的。首先,我們對(duì)MyDataContext做一個(gè)擴(kuò)展:
然后我們?cè)僦谱饕粋€(gè)HttpModule(并且在web.config里面配置好):
接下來(lái),我們只要在邏輯層這么直接寫(xiě)即可:
這么改造完之后,你會(huì)發(fā)現(xiàn)幾乎可以在所有地方直接返回IEnumerable(除了有的時(shí)候Linq to Sql本身有Bug),整個(gè)邏輯層內(nèi)的設(shè)計(jì)變得簡(jiǎn)單化:一開(kāi)始檢查各種參數(shù)(是否具備完整或者部分權(quán)限),然后根據(jù)檢查結(jié)果做完全信賴(lài)的操作。由于返回的是實(shí)體對(duì)象,或者IEnumerable,幾乎所有重復(fù)性的Sql調(diào)用也隨之自然消失了。如果有所懷疑的話,您可以用Sql Profiler自行做修改前后的對(duì)比,看看效果是否“驚人”?
也許有人會(huì)質(zhì)疑,這樣好嗎?豈不是通過(guò)user.Company.Transactions就可以得到所有的Transaction了?沒(méi)錯(cuò),如果所有東西都是公開(kāi)的話,就會(huì)有這個(gè)問(wèn)題。如果要徹底解決這樣的問(wèn)題,需要將這些部分變成對(duì)邏輯層可見(jiàn),而對(duì)其它層不可見(jiàn)的修飾方式——比如兩層在一個(gè)dll里面,這些屬性是internal的,或者放在兩個(gè)dll里面并且打上InternalsVisibleTo標(biāo)記。通過(guò)這種方式,就可以避免上層直接查找DAL中一些在BLL中需要經(jīng)過(guò)權(quán)限檢查才可以得到的內(nèi)容。當(dāng)然,如果項(xiàng)目比較小的情況下,你也可以選擇不要這么麻煩,直接控制代碼質(zhì)量即可(要求有些東西必須通過(guò)BLL來(lái)獲得)。
_________________________________________
后記:
因?yàn)橹虚g跳過(guò)了一些產(chǎn)生問(wèn)題的步驟,引起了不少的誤解,這里特別解釋清楚:
1、原方案因?yàn)椴辉谕粋€(gè)Context之下,所以返回的實(shí)體對(duì)象是不分功能失效的。考慮:
_user = BLL.GetUser(HttpContext.Current.User.Identity);
這樣的緩存,由于DataContext在GetUser中已經(jīng)拋棄了,因此,_user.Company這樣的訪問(wèn)就會(huì)報(bào)錯(cuò)。最終你更可能選擇,要么全面放棄實(shí)體對(duì)象之間的關(guān)聯(lián)屬性,要么就只是緩存userId。無(wú)論哪一種方案,都意味著,當(dāng)你緩存userId的時(shí)候,是不會(huì)自動(dòng)緩存對(duì)應(yīng)的CompanyInfo的。(這么說(shuō)明報(bào)了吧?這才是我說(shuō)的,可以自動(dòng)緩存的意思。)
關(guān)于這一部分的誤解,回復(fù)中有一個(gè)例子:
public static T_User TestMethod2()
{
return dbUserDataContext.CurrentHttpContext.T_User.FirstOrDefault();
}
//調(diào)用代碼
for (int i = 0; i < 5; i++)
{
BLL.dbUser.TestMethod2();
}
這樣顯然是會(huì)重復(fù)發(fā)出Sql調(diào)用的,這不是我說(shuō)的場(chǎng)景,我說(shuō)的是:
public static T_User TestMethod2()
{
return dbUserDataContext.CurrentHttpContext.T_User.FirstOrDefault();
}
//調(diào)用代碼
_user = BLL.dbUser.TestMethod2(); // 緩存
for (int i = 0; i < 5; i++)
{
Debug.Writeline(_user.Company.CompanyName); // 這個(gè)時(shí)候Company就不會(huì)不停的調(diào)數(shù)據(jù)庫(kù)
}
從這個(gè)角度來(lái)講,我并沒(méi)有討論如何緩存,而是討論的如何便于緩存,如果在不需要過(guò)高性能要求的情況下,不動(dòng)用完整的、同時(shí)也很復(fù)雜的緩存機(jī)制。
2、我們?cè)倏戳硗庖粋€(gè)例子:
public List<Item> GetItemsForListing(int ownerId)
{
ItemDataContext dataContext = new ItemDataContext();
var query = from item in dataContext.Items
where item.UserID == ownerId
orderby item.CreateTime descending
select item;
return query.ToList();
}
大家可以看到,這個(gè)設(shè)計(jì)里面,返回至十一個(gè)ToList(),也就意味著已經(jīng)從數(shù)據(jù)庫(kù)中取出所有對(duì)象了。但是有的時(shí)候,我們實(shí)際上希望在這個(gè)基礎(chǔ)上再進(jìn)一步收縮。在這種情況下,我們就不能很好的重用原來(lái)的代碼了,因?yàn)檫@個(gè)函數(shù)返回的是一個(gè)組內(nèi)存對(duì)象,而不是一個(gè)IQueryable的表達(dá)式。那么我們要么在這個(gè)List<Item>結(jié)合中在內(nèi)存中進(jìn)行過(guò)濾,要么重新寫(xiě)一個(gè)方法來(lái)處理。無(wú)論如何,這都導(dǎo)致了BLL層的臃腫。如果是如下的寫(xiě)法呢:
public IQueryable<Item> GetItemsForListing(User owner)
{
ItemDataContext dataContext = new ItemDataContext();
var query = from item in dataContext.Items
where item.UserID == owner.Id
select item;
return query;
}
注意,我連order by 也去掉了,因?yàn)檫@完全可以在更上一層再?zèng)Q定如何處理。同樣的,我也可以在這個(gè)返回值得基礎(chǔ)上繼續(xù)做進(jìn)一步的篩選。而這種篩選無(wú)論在BLL還是更高層次來(lái)做,都會(huì)有著更高的效率。(說(shuō)到這里,我也不得不擔(dān)心,有人會(huì)說(shuō),這種篩選應(yīng)該被封閉起來(lái)。這部分是另外一種討論了:BLL到底應(yīng)該封裝到什么程度。這里不作過(guò)多的爭(zhēng)論了,我們假定輕量級(jí)的BLL更有靈活性。不過(guò)如果非要爭(zhēng)論,我會(huì)說(shuō):首先,原來(lái)的設(shè)計(jì),你也無(wú)法避免UI層用這個(gè)函數(shù)的返回值來(lái)做自己的篩選和處理;其次,我的設(shè)計(jì),也完全可以在BLL提供進(jìn)一步的標(biāo)準(zhǔn)篩選函數(shù)。因此這也并非我要討論的內(nèi)容。)
那么,這么做有什么好處呢?最大的好處是,提供同樣級(jí)別功能的BLL情況下,我的設(shè)計(jì)更可能只去所需要的數(shù)據(jù)——嚴(yán)格的說(shuō)應(yīng)該限定為,在只訪問(wèn)一次數(shù)據(jù)庫(kù)的情況下。其他情況下也有可能和原來(lái)設(shè)計(jì)一樣,需要依賴(lài)緩存來(lái)提高性能。不過(guò)實(shí)戰(zhàn)情況下,可以發(fā)現(xiàn)這么做之后一般都是獲取更少的數(shù)據(jù)。
無(wú)論如何,減少?gòu)臄?shù)據(jù)庫(kù)取出的不必要數(shù)據(jù),肯定是一件好事。如果加上緩存機(jī)制的話,更少需要緩存的數(shù)據(jù),也可能提供更高的效率。
3、有一點(diǎn)不需要多說(shuō)了,就是如果要加入緩存機(jī)制的話,在同一個(gè)Context下面肯定比在不同Context下面好處理得多,最簡(jiǎn)單的一個(gè)原因,就是不需要考慮Detach和Attach的問(wèn)題(偏偏Linq2Sql不存在Detach的方法,除非你自己做每一個(gè)對(duì)象的深拷貝工作,這拜Linq2Sql的Tracker所賜,具體這也不討論了)。
回復(fù)中還有一種說(shuō)法,認(rèn)為可以盡量將操作都封裝到BLL層,例如:
using (new context)
{
receiveRelatedData()
using (new tansation)
{
action1 (context);
action2 (context);
action3 (context);
}
}
例子中,并沒(méi)有很清晰的指出,是整個(gè)代碼在BLL中呢,還是只有receiveRelatedData和actions是BLL。無(wú)論如何,我想這是另一個(gè)問(wèn)題了,就是BLL應(yīng)該復(fù)雜到什么程度。我個(gè)人的理念是:在保證邏輯完整的情況下,盡量提供最小的原子操作,讓上層自由互相組合。這樣可以避免BLL越來(lái)越臃腫,甚至有一些功能會(huì)逐漸被拋棄,進(jìn)而被遺忘的問(wèn)題。例如,我會(huì)提供獲取當(dāng)前用戶(hù)、某用戶(hù)購(gòu)買(mǎi)一個(gè)產(chǎn)品,這樣兩個(gè)原子操作,來(lái)達(dá)到當(dāng)前用戶(hù)購(gòu)買(mǎi)產(chǎn)品的目的。否則,可以想象,讓如果要讓管理員強(qiáng)制另一個(gè)用戶(hù)購(gòu)買(mǎi)一個(gè)產(chǎn)品,將會(huì)需要另一整套的邏輯。而按照我的思路,只要設(shè)計(jì)一個(gè)管理員獲取另一個(gè)管理員,以及代理權(quán)限系統(tǒng)即可,而不需要連后面的購(gòu)買(mǎi)過(guò)程也封裝起來(lái)。或者說(shuō),前一種思路容易導(dǎo)致BLL函數(shù)數(shù)量呈N*M的方式增長(zhǎng),而我的思路則是盡可能讓BLL按照N+M的方式增長(zhǎng)。(這個(gè)如果要討論的話,還是另外開(kāi)篇把,這里可能說(shuō)不完。)
不過(guò)即使是這樣,我也沒(méi)有看出為何不可直接將Context緩存起來(lái)?比如:
receiveRelatedData(CurrentContext)
using (new tansationScope())
{
action1 (CurrentContext);
action2 (CurrentContext);
action3 (CurrentContext);
}
還是一樣可以跑得通吧?
簡(jiǎn)而言之,本文并非否認(rèn)緩存機(jī)制的作用,更不是要替代緩存機(jī)制。與之正好相反,這里面是要讓緩存變得更容易一些,同時(shí)在一定范圍內(nèi),提高不使用復(fù)雜緩存機(jī)制時(shí)的效能,推遲系統(tǒng)變得更復(fù)雜的時(shí)間。

浙公網(wǎng)安備 33010602011771號(hào)