前言
又是一個炎熱夏日的晚上,開着空調聽着音樂又開始了我們今天的博文。此文並不是ROM工具哪家強之類的引戰貼,只是本文自己的一點看法和見解,望前輩看官有望斧正
聲明
本文歡迎轉載,原文地址:http://www.cnblogs.com/DjlNet/p/7220720.html
開始正文
話說回顧歷史的話,在linq to sql的年代到后面linq to entity也就是ef4.1以至於現在的ef6.1.3歷經了好些歲月的打磨,且也用ef6在真實項目中使用體會到了在開發速度和維護成本體現出來的優勢,當然並不代表它就是沒有弱點或者是說要通過一些方式去規避不當使用造成的問題哈,所以一個東西不可能做到盡善盡美,畢竟在我看來它也只是基於C#3開始帶來linq基礎的上面的一層數據庫訪問的抽象,當然不光是查詢啦,也包含寫操作啦或者事務之類的,反正啦上層抽象只能做到它自己功能的抽取也就是包含關系型數據庫大家都有的一些特性和操作,這樣的抽象也是合情合理所以就在linq的基礎上面就這樣孕育而生,以及具體的話就需要交給數據庫對應的提供器去解析表達式去適配數據庫自個兒的一些特點和異同,這也是復雜和小心的過程因為這基本都關系着到達數據庫執行的sql語句到底如何的問題,實際反應的就是產生的執行計划消耗等等,這其中也是其他ORM工具(暫且這樣稱呼吧,例如一些Dapper+Extension之類說實話有點勉強,注意這里並沒有故意貶低牛逼的Dapper的意思,畢竟我大Stackoverflow就是用的它,具體可以去github查看wiki)同樣需要去做的一件事兒,當然這里不排除微軟老爹對自家數據庫sql server親兒子有些小操作。
這里要額外提一下:感謝@Pomelo大大對MySql實現ef core驅動的奉獻,也得到了微軟的支持,所以應該可以放心食用
,微軟官方目前的數據庫驅動列表:https://docs.microsoft.com/zh-cn/ef/core/providers/,其中ef core對於sql server有個批量操作的優化,偶爾看看issues得知,同時發現同事園子一老哥也發文說明了情況,具體傳送門:https://github.com/aspnet/EntityFramework/issues/9270,這個貌似是一直被業界所詬病的問題,當然也可以自定義擴展或者特殊情況配合sql+transaction來處理批量寫問題我覺得也是可以的,以至於一些第三方ef batch框架(例如:entity framework plus)這里暫不評價了,個人覺得畢竟帶來方便的同時引入了復雜度
好了屁話說了一大推,說到這里你們肯定會以為又是一個EF長篇大論文入門文,例如什么是ORM,什么是OTO,什么是Code First,這些介紹的太多太多了,建議還是才看微軟ef官方文檔即可(https://msdn.microsoft.com/en-us/library/ee712907(v=vs.113).aspx)以及微軟新上線的文檔地址:https://docs.microsoft.com/en-us/ef/,也許能把官方文檔大致瀏覽一邊基本做到心中有數遇到問題再去翻翻即可,比我在這里BB可能或許有用,所以這里並不會對EF有一個詳細的解讀(當然做到詳細解析,我也做到不到呀),這方面的博文已經數不勝數,在這里可能針對性的看某些問題發表一些個人的看法或者見解。其中回憶起來的問題以及經常園子討論的問題包含如下:【可能想的不全后面可以更新】
1、EF的數據庫上下文實例的生命周期管理的標准實踐 ?
2、EF的數據庫連接打開和關閉的時間點和管理是怎么回事 ?
3、EF為何第一次啟動這么慢和怎么解決 ?
4、EF在批量操作時對象跟蹤時性能問題該如何解決 ?
5、EF批量數據時怎么提高速度和保證事務 ?
6、EF對復雜的查詢表達式解析能力如何?
7、開發者怎么審查EF翻譯的SQL語句?
8、開發者怎么監控EF在網站運行情況?
9、......
等等,可能問題還有很多這里暫時沒有收錄。接下來就是需要我們逐一對問題思考和解析,注意:可能這里的理解有主觀意識,當然某些問題我也會用實驗例子來證實大致的論述。
1、EF的數據庫上下文實例的生命周期管理的標准實踐 ?
分析:由於ef的讀寫操作都是基於DbContext數據庫上下文來操作的,所以當進行這些操作的時候,就需要一個對象實例才可以,那么問題就在於我可以在同一個對象操作多次嗎,什么時候該創建這個對象以及什么時候結束它,這個對象存在多個有什么影響嗎,對象實現了IDisposable接口我必須要在using中使用嗎,好,這里我假設我們是處於web應用程序中(通常情況也是如此),基於http請求來討論這個問題
解答:
(a)一個(同一個)對象本身就是可以多次連接訪問數據庫的包含了讀寫可能會多次打開和關閉數據庫連接,當然這里是基於ado.net數據庫連接池的無須過於擔心,所以多次操作本身就是合情合理的,也是必須的
(b) 創建對象的時刻問題,上下文得知,基於http請求的話,每次請求可能會有數據庫訪問的需求(大致都有這個可能),那么每次請求什么時候需要創建一個DbContext對象呢,其實在你的程序結構中,無非就是在Controller或者Service或者Repository中包含(或者說成注入更恰當)DbContext對象的本身被實例化的時候創建DbContext,結束無非也就是1、在包裹類釋放時跟着釋放(多實例模式)2、在請求結束的時候釋放(請求期間共享實例模式)
這里說點題外話:通常意義上來說,把這些這些對象通常由Ioc容器統一(例如: Unity Autofac Ninject等等)來創建和管理生命周期這樣來得更合適些,拿unity舉例說明(當然這里也可以作為一個私有字段存在於你的Repositotry或者Service中都看你自己選擇):這里我們可以自定義個IDbContext接口讓DbContext實現它這樣這是為了方便注入**
container.RegisterType<IDbContext, DJLNETDBContext>(new PerRequestLifetimeManager());
,然后設置為 PerRepuestLifetimeManager每次請求生命周期(基於HttpModule來實現的)Microsoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule(typeof(UnityPerRequestHttpModule));
**,來實現了請求單例,其他框架同樣具備類似功能,類似asp.net core中AddScoped的功能。這樣做的好處在於在一個請求期間就可以共享一個DbContext對象啦,既可以讓局面變得清晰和簡單,又可以減少堆資源的消耗
(c)多個DbContext對象對於asp.net本身就是基於並發應用程序而言這種就是合理,且多個對象在不同的請求線程中也是相互不受干擾和影響的,可能也就是對於同一時刻並發請求數過多對於內存占用可能稍有提升,這不是你需要擔心的問題,因為會釋放的。
(d)IDisposable在我理解看來是提供給需要釋放非托管資源的對象實現的,所以想當然以為DbContext在實現中有這種操作,通過翻源碼和多方作證得知,並非如此(這里提前說明一下 1、使用using是因為一來為了准許一種契約或者模式二來為了保證安全性 2、至於connection的管理是DbContext自己的事情會自動處理好),詳情請看問題2中有相應解析,所以不需要using已然可以使用它完成你應有的操作。
總結: 如果是EF,建議將其數據庫實例上下文DbContext設置為請求期間共享一個實例對象保證創建和銷毀操作保證按照您的預期執行,至於實現看自己喜好拉,不過交給一些現成的ioc框架去實現,不乏是一個不錯的選擇!
2、EF的數據庫連接打開和關閉的時間點和管理是怎么回事 ?
思考:如果是你怎么設計關於數據庫連接對象的管理(這里的管理是連接對象的打開和關閉,並不是創建和銷毀,畢竟close和dispose還是有區別的)更加合適呢?
解析: 首先上面1的的(d)也提到了關於using的問題同樣也涉及connection,先讓我來看一篇老外的博文:http://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext/可能博文有點老了,但是在 Diego Vega回信中我們發現關鍵信息(加粗標記關鍵信息):
The default behavior of DbContext is that the underlying connection is automatically opened any time is needed and closed when it is no longer needed. E.g. when you execute a query and iterate over query results using “foreach”, the call to IEnumerable
.GetEnumerator() will cause the connection to be opened , and when later there are no more results available, “foreach” will take care of calling Dispose on the enumerator, which will close the connection. In a similar way, a call to DbContext.SaveChanges() will open the connection before sending changes to the database and will close it before returning.
Given this default behavior, in many real-world cases it is harmless[無害的] to leave the context without disposing it and just rely on garbage collection.
That said, there are two main reason our sample code tends to always use “using” or dispose the context in some other way:
- The default automatic open/close behavior is relatively easy to override: you can assume control of when the connection is opened and closed by manually opening the connection. Once you start doing this in some part of your code, then forgetting to dipose the context becomes harmful, because you might be leaking open connections.
- DbContext implements IDiposable following the recommended pattern, which includes exposing a virtual protected Dispose method that derived types can override if for example the need to aggregate other unmanaged resources into the lifetime of the context.
By the way, with DbContext the pattern to open the connection manually and override the automatic open/close behavior is a bit awkward:
((IObjectContextAdapter)dbContext).ObjectContext.Connection.Open()
But we have a bug to make this easier as it used to be with ObjectContext before, e.g.:
dbContext.Database.Connection.Open()
總結: EF自身已經自動的去控制連接對象的開關了,也就是當你去迭代查詢對象或者保存修改時會在恰當的時候(具體看上文的時間點黑體部分)幫你打開或者關閉連接,這樣就算不明顯去調用dispose,讓GC去管理DbContext的剩余托管資源也是無害,至於后半段只是解釋下為何還是要實現IDispose接口以及存在的必要和安全性,其實作為編碼的我們是可以做到避免手動去控制連接對象,using只是作為最后的一道屏障而已。鑒於不能僅憑一片博文定論,可以參考一下源碼中dispose中的實現:https://github.com/aspnet/EntityFramework6/blob/master/src/EntityFramework/DbContext.cs
3、EF為何第一次啟動這么慢和怎么解決 ?
解析: 來先看看關於這個問題老外反饋的issues(包含老外的做的實驗):https://github.com/aspnet/EntityFramework/issues/4372
以及dudu園長解釋的原因所在以及解決方案:http://www.cnblogs.com/dudu/p/entity-framework-warm-up.html 在備注一下從IIS入手的如何優化 EF的博文地址:http://www.cnblogs.com/lkd3063601/p/4713637.html和從GAC入手優化的 https://www.fusonic.net/en/blog/3-steps-for-fast-entityframework-6.1-code-first-startup-performance/
總結 總的來說這里都是搬運工拉,記錄備注一下,當然EF這樣設計也是合乎情理,在第一次訪問之后緩存下來映射視圖和一些元數據相關的東西,以后直接復用
4、EF在批量操作時對象跟蹤時性能問題該如何解決 ?
分析: 這里先說下對象跟蹤,默認EF是啟動對象跟蹤的,也就是context.Configuration.AutoDetectChangesEnabled = true;
當發現使用查詢出來的對象屬性值變更之后,在DbContext.SaveChanges
的時候這里會主動觸發去檢查對象的CurrentValues與OriginalValues的差異然后標記為Modified狀態,當我們有成百上千的對象需要去修改或者添加,對應 AddRang 或者 RemoveRang 的時候就會觸發很多次對象跟蹤,所以這是一個壞操作,參考https://msdn.microsoft.com/en-us/library/jj556205(v=vs.113).aspx官方示例做法如下:
using (var context = new BloggingContext())
{
try
{
context.Configuration.AutoDetectChangesEnabled = false;
// Make many calls in a loop
foreach (var blog in aLotOfBlogs)
{
context.Blogs.Add(blog);
}
context.SaveChanges();
}
finally
{
context.Configuration.AutoDetectChangesEnabled = true;
}
}
注意這里有個官方提示:Don’t forget to re-enable detection of changes after the loop — We've used a try/finally to ensure it is always re-enabled even if code in the loop throws an exception.
5、EF批量數據時怎么提高速度和保證事務 ?
分析解答: 一直以來這就是一個焦點問題,問題也是在於為何插入很多數據和修改很多數據的時候很慢,怎么解決這個問題以及還需要保持和原先邏輯(寫邏輯)在一個事務里面等,當然你依然可使用context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
這樣的配置去提高代碼層面的優化,這里的優化針對批量添加刪除修改都是有效的,下面的System.Data.SqlClient.SqlBulkCopy
針對添加當然也指定了使用環境sql server畢竟是數據庫本身支持才可以,不過這里的基本和EF無關了只是提供批量插入的一個途徑而已。
那么加上事務是否可以快點呢?答案是肯定的,使用Database.BeginTransaction()
或者System.Transactions.TransactionScope
將ef操作裹起來在SaveChanges之后,如若沒發生異常打包提交在速度上面會有大幅度提升,相對於不顯示使用事務機制,而使用EF默認事務機制的情況上面比較得出上面的結論,這個好處也得益於數據庫本身的支持
除了批量添加對象可以忽略,但是我批量刪除對象和批量修改對象則需要那拿到那些需要做此操作的源數據集合才可以,這里就需要先查詢出來這些對象,然后遍歷刪除或者修改他們的屬性,且還需要保持這些對象是被跟蹤的對象,那么我可以不查詢出來這些對象也想刪除或者修改它們?
答案:可以的,其中基於目前的情況有兩種做法,首先第一種也是我比較推薦的一種做法或者說是官方做法:https://msdn.microsoft.com/en-us/library/dn456843(v=vs.113).aspx
這里搬運一點官方代碼(詳情請參考官方代碼):
using (var context = new BloggingContext())
{
using (var dbContextTransaction = context.Database.BeginTransaction())
{
try
{
context.Database.ExecuteSqlCommand(
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'"
);
var query = context.Posts.Where(p => p.Blog.Rating >= 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
context.SaveChanges();
dbContextTransaction.Commit();
}
catch (Exception)
{
dbContextTransaction.Rollback();
}
}
}
在不引入第三方框架的情況下,能夠自己清晰的掌握代碼以及處理方法,這是比較好的,並且能夠和你想的處理結果一致,第二種做法: 即使用一些基於EF的第三方擴展,這里就展示了,因為本身對其不還不是很了解,其實我個人覺得處理20%的需求但是又不想引入新的nuget包的情況,這樣特殊處理也是可以的,再加上一點自己的封裝例如:實現AOP層面的事務封裝,這樣在你ExecuteNonQuery();
與SaveChanges
就不會明顯的感覺到事務的存在從而把問題簡化,抽到公共邏輯當中去
總結與后續
一 一,為何在問題5之后就沒了呀,關於后面
6、EF對復雜的查詢表達式解析能力如何?
7、開發者怎么審查EF翻譯的SQL語句?
8、開發者怎么監控EF在網站運行情況?
這幾個問題將會在下一篇博文給出分析和解讀,並不是故意賣關子是博主本身還沒有准備好下一篇文的內容哈,望各位原諒,關於以上的問題和解析及其回答屬於個人意見,如有不對的地方歡迎討論哈
【2017年8月3日21:50:50】
划重點拉,順便再補充一下示例代碼
后記
在下先干為敬:
不哭長夜者,不足以與人生。不曾為夢想奮斗,拿什么去燃燒青春。有夢之人亦終將老去,但少年心氣如昨。