在一次請求中,即一個線程內,若是用到EF數據上下文對象,就創建一個,這也加是很多人的代碼中習慣在使用上下文對象時,習慣將對象建立在using中,也是為了儘早釋放上下文對象, 但是如果有一個業務邏輯調用了多個dal層的方法,交互資料庫多次,這樣效率會低一些,而且在使用EF的情況下,我們通常把SaveC ...
在一次請求中,即一個線程內,若是用到EF數據上下文對象,就創建一個,這也加是很多人的代碼中習慣在使用上下文對象時,習慣將對象建立在using中,也是為了儘早釋放上下文對象, 但是如果有一個業務邏輯調用了多個dal層的方法,交互資料庫多次,這樣效率會低一些,而且在使用EF的情況下,我們通常把SaveChange這個方法提到業務邏輯層(下文中會提到),不保證同一個業務邏輯使用的是同一個上下文對象,事務,工作單元模式將無法實現。而且可能造成數據混亂,每次創建的對象執行相應的資料庫操作,與此同時,同一次的請求可能包含對數據的不同操作。其他的EF對象內獲得的數據可能已經是“過期”的了。即這個數據已經變動過。這就是臟讀。
為瞭解決這個問題,關鍵就是上下文對象的創建問題。
這裡首先想到單例模式,不過在這裡,不適合用,原因是使用單例模式,會使EF對象得不到及時的資源釋放。想象一下,無數個請求對資料庫的訪問,DbContext對象容器無數次增加對Model對象的Attach監控,記憶體就爆了。
優化就是折中的過程,所以第二種方式考慮保證線上程內對象唯一,對於每一個請求使用同一個上下文。如何保證呢,通過微軟ASP機制線程相關的HttpContext對象以及CallContext對象。前面一篇文章中說過,HttpContext機制其實就是依靠CallContext對象實現的。先來看使用CallContext解決這個問題
你可以這樣做,在網站Common中添加處理類:
[csharp] view plain copy
- /// <summary>
- /// 用來創建EF上下文對象,且保證線程內唯一。
- /// </summary>
- public class DbContextFactory
- {
- //DbContext在System.Data.Entity;中,不過這裡直接只引用這一個不行,還有EF其他的一些NameSpace所以直接添加一個實體模型,所有引用都進來了,然後再把模型刪了
- public static DbContext GetDbContext()
- {
- DbContext dbContext = (DbContext)CallContext.GetData("dbContext");
- if (dbContext == null)
- {
- dbContext = new WebEntities();
- CallContext.SetData("dbContext", dbContext);
- }
- return dbContext;
- }
- }
是不是很像緩存的使用策略。
仔細思考一陣後發現,上面使用CallContext來存儲有什麼問題?就是說上面是把上下文對象依賴於一個線程。那麼由於線程池的存在,線程在處理完一個請求之後,並沒有被銷毀,存儲在CallContext中的上下文對象也一直存在,如果是下一次拿出這個線程去處理另一個請求,這個上下文對象其實也在不斷的膨脹,只不過比全局的膨脹的稍微慢一些。而且,有時候一個線程並不一定是拿去處理請求了,如果是伺服器拿去處理其他的業務,那就可能引發一些其他的問題。
所以,改進一下上面的辦法,借鑒一下J2EE的hibernate和mybatis,在DbContextFactory中添加一個remove方法,在業務邏輯層中每次請求使用完上下文之後,就把它從線程中移除。
解決了,可是這辦法實在是。。。那如果我一次請求要調幾次業務邏輯呢,還是要創建多次上下文。而且這樣手動管理的方式,讓人痛苦。相信也是由於這個原因,在spring+hibernate中大家也是更願意用HibernateTemplate而不是HibernateDaoSupport。
其實我們還有更好的辦法,在HttpContext中有一個Items屬性,它也可以用來保存key-value,這就完美了,一次請求正好對應著一個HttpContext,請求結束,它自動釋放,EF上下文也就不存在了。把上面代碼中的CallContext改為HttpContext.Current.Items,OK。
[csharp] view plain copy
- public static DbContext DbContext()
- {
- DbContext dbContext = HttpContext.Current.Items["dbContext"] as DbContext;
- if (dbContext == null)
- {
- dbContext = new WebEntities();
- HttpContext.Current.Items["dbContext"] = dbContext;
- }
- return dbContext;
- }
再說SavaChanges這個方法,我們現在可以做到EF上下文創建的優化,那麼它對資料庫的交互呢?這是我們寫了無數次的方法:
[csharp] view plain copy
- public int AddUser(User user)
- {
- context.Add(user);
- return context.SaveChanges();
- }
當我們使用一個業務邏輯複雜的方法中,它可能需要使用到多個dal層對象或者說調用多次dal層的方法,上面的寫法,調幾次,EF就與資料庫交互了幾次,效率還是很低。那我們何不把與資料庫的交互方法SaveChanges()提到bll層來調用,由bll層方法來調用,一次的業務邏輯,只交互一次,形成一種工作單元模式。
那麼怎麼提取,由於我們上下文對象在請求內唯一,那麼就再簡單不過了。
[csharp] view plain copy
- public class DbSession
- {
- public static int SaveChanges()
- {
- return DbContextFactory.GetDbContext().SaveChanges();
- }
- }
為什麼把這個類名取為DbSession,學習JavaEE的朋友可能馬上想到了MyBatis,Hibernate,我們封裝了一個對資料庫的單元操作,與資料庫進行交互,就是一次與資料庫的會話。
另外,我剛接觸EF的時候就有這個疑問,EF如果做到事務的處理,用TransactionScope或DbConnection?大可不必,如果我們把SaveChanges()提到業務邏輯層,就組成了一個事務單元,再聯想一下spring,為什麼會把聲明式事務放在Service層而不是Dao層,而且SaveChanges()這個方法其實本身事務的特性,如果保持了上下文對象的唯一性,間接也是完成了事務單元。
===========================2016-11-7===========================
最近在MVC裡面用了一下NHibernate,仍然需要像管理EF上下文一樣管理Session對象,同樣,我們也可以把它"緩存"在HttpContext中,但是NHibernate已經幫我們完成了類似的工作。詳情參見http://www.cnblogs.com/13yan/archive/2013/05/17/3083552.html。作者還給大家提供了一個NHibernateHelper,很贊。