(譯者注:使用EF開發應用程序的一個難點就在於對其DbContext的生命周期管理,你的管理策略是否能很好的支持上層服務 使用獨立事務,使用嵌套事務,並行執行,異步執行等需求? Mehdi El Gueddari對此做了深入研究和優秀的工作並且寫了一篇優秀的文章,現在我將其翻譯為中文分享給大家。由於原文太長,所以翻譯后的文章將分為四篇。你看到的這篇就是是它的第二篇。原文地址:http://mehdi.me/ambient-dbcontext-in-ef6/)
DbContext的默認行為
通常來說,DbContext的默認行為可以被描述為:“默認情況下就能做正確的事”。
下面是你應該記在腦海里面的幾個關於EntityFramework的重要行為。這個列表描述了EF訪問SqlServer的行為。用其它的數據庫可能會略有差異。
DbContext不是線程安全的
你千萬不要從多個線程同時去訪問DbContext派生類實例。這可能導致將多個查詢通過一個相同的數據庫連接被同時發送了出去——它將破壞DbContext維護的一級緩存的狀態——它們被用來提供標識映射(Identity Map),變更追蹤和工作單元的功能。
在一個多線程應用程序中,你必須為每一個線程創建一個獨立的DbContext派生類實例。
問題來了,如果DbContext不是線程安全的,那么它怎么支持EF6的異步功能呢?其實很簡單:只需要保證在任何時刻只有一個異步操作被執行(就像EF的支持異步模式的規范描述的那樣)。如果你嘗試在同一個DbContext實例上並發的執行多個操作,比如通過DbSet<T>.ToListAsync()方法並發地執行多個查詢語句,你將會得到一個帶有下面消息的NotSupportedException。
A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.
EF的異步功能是為了支持異步編程模型,而不是並發編程模型。
當且僅當SaveChanges()方法被調用的時候,修改才會被持久化
任何對實體的修改,包括更新,插入或者刪除,當且僅當DbContext.SaveChanges()被調用的時候才會被持久化到數據庫。如果DbContext實例在SaveChanges()方法被調用之前就被釋放掉了,那么這些更新操作,插入操作,刪除操作沒有一條能持久化到底層數據庫。
下面是用EF來實現一個業務事務的規范方式:
using (var context = new MyDbContext(ConnectionString)) { /* * 業務邏輯放在這兒. 通過context添加,修改,刪除數據。 * * 拋出任何異常就可以回滾所有變化。 * * 直到業務事務完成,否則不能調用SaveChanges()方法 * 也就是說不能部分或者中間保存。 * 每一個業務事務只能剛好調用一次SaveChanges()方法 。 * * 如果你發現你自己需要在一個業務事務里面多次調用 * SaveChanges()方法,那就意味着你在一個服務方法 * 里面實現多個業務事務。這絕對是災難的“必備良葯”。 * 調用你的服務的客戶端會很自然的假定你的服務方法 * 以原子的行為提交或者回滾——但你的服務卻可能 * 部分提交,讓系統處於一個不一致的狀態。 * * 在這種情況下,將你的服務方法重構成多個服務方法—— * 每一個服務方法剛好實現了剛好一個業務事務。 */ [...] // 完成業務事務並且持久化所有變化 。 context.SaveChanges(); // 在這行代碼之后變化不可能回滾了。 // context.SaveChanges()應當是任何業務事務 // 的最后一行代碼。 }
NHibernate用戶注意事項
如果你擁有NHibernate背景,那么可以告訴你的是EF將變化持久化到數據庫的方式是它與NHibernate的最大不同。
在NHibernate中,Session操作默認情況下處於AutoFlush模式。在這種模式下,Session將在執行任何‘select’操作之前自動將所有變化持久化到數據庫——確保已持久化到數據庫的實體和它們在Session中的內存狀態保持一致。對NHibernate來說,EF的默認行為相當於將Session.FlushMode設置為Never。
EF的這個行為可能會導致一些微妙的bug——查詢意外的返回過時的或者不正確的數據。默認情況下NHibernate是絕不可能出現這種情況的。但從另外一方面來說,這卻又極大的簡化了數據庫事務管理的問題。
在NHibernate中最棘手的問題之一就是正確的管理數據庫事務。由於NHibernate的Session可以在它的整個生命周期中的任何時間點自動地將未持久化的變化持久化到數據庫,並且可能在一個業務事務里面持久化多次——這兒沒有一個定義良好的點或者方法來開啟數據庫事務以確保所有的修改以原子的行為提交或者回滾。
在NHibernate中正確管理數據庫事務的唯一可靠方法就是將你的所有服務方法打包在一個顯式數據庫事務中。這就是大部分基於NHibernate的應用程序的處理方式。
這種方式的負面效應就是它要求打開一個數據庫連接和事務的時間比實際需要的要更長——因此增加了數據庫鎖的競爭和數據庫死鎖發生的可能性。開發者也很容易不經意的執行一個長時間計算或者一個遠程服務方法的調用而沒有意識到甚至根本就不知道他們是在一個數據庫事務打開的上下文中。
EF的方式——只有SaveChanges()方法必須被打包在一個顯式數據庫事務中(當然使用一個REPEATABLE READ 或者SERIALIZABLE隔離級別的情況例外),保證了數據庫連接和事務保持盡可能的短暫。
使用自動提交事務(AutoCommit transaction)來執行讀取操作
DbContext不支持打開一個顯式事務來執行讀取操作。它依賴於SQL Server的自動提交事務(Autocommit Transaction) (或者 隱式事務(Implicit Transaction)——如果你啟用了它們的話,但那相對來說不是常見的操作)。自動提交事務(或者隱式事務)將會使用數據庫引擎被配置的默認事務隔離級別(對SQL Server來說就是READ COMMITTED)。
如果你已經工作有一段時間,尤其是如果你以前使用過NHibernate,那么你可能聽說過“自動提交事務(或者隱式事務)是糟糕的”。實際上,依賴於自動提交事務的寫操作可能在性能上產生災難性影響。
但對於讀操作來說情況就大不一樣了。你可以跑下面的SQL腳本親自去看看。對select語句來說,自動提交事務或者隱式事務都不會有任何明顯的性能影響。
/* * 用自動提交事務,隱式事務,顯式事務分部執行10000 * 次select查詢. * * 這些腳本假定數據庫包含一張Users表,它有一個列名為Id * 類型為INT的列。 * * 如果你在SQL Server Management Studio里面運行的話 * 右鍵查詢窗口,進入查詢選項 -> 點擊結果並勾選 * “執行后放棄結果”。否則你的測試結果將會被網格的 * 刷新驗證影響 */ --------------------------------------------------- -- 自動提交事務 -- 6 秒 DECLARE @i INT SET @i = 0 WHILE @i < 100000 BEGIN SELECT Id FROM dbo.Users WHERE Id = @i SET @i = @i + 1 END --------------------------------------------------- -- 隱式提交事務 -- 6 秒 SET IMPLICIT_TRANSACTIONS ON DECLARE @i INT SET @i = 0 WHILE @i < 100000 BEGIN SELECT Id FROM dbo.Users WHERE Id = @i SET @i = @i + 1 END COMMIT; SET IMPLICIT_TRANSACTIONS OFF ---------------------------------------------------- -- 顯示事務 -- 6 秒 DECLARE @i INT SET @i = 0 BEGIN TRAN WHILE @i < 100000 BEGIN SELECT Id FROM dbo.Users WHERE Id = @i SET @i = @i + 1 END COMMIT TRAN
很顯然,如果你需要用一個比默認READ COMMITTED更高的隔離級別的話,那么所有讀操作都將是顯式數據庫事務的一部分。在那種情況下,你需要自己開啟事務——EF將不會為你做這個。但這通常只會為指定的業務事務做特別處理。EF的默認設置能適合大部分業務事務。
使用顯式事務來執行寫操作
EF通過DbContext.SaveChanges()方法自動地將所有操作打包在一個顯式數據庫事務里面——以確保應用在context的所有修改要么完全提交要么完全回滾。
EF寫操作使用數據庫引擎配置的默認事務隔離級別(對SQL Server來說就是READ COMMITTED)。
NHibernate用戶注意事項
這是EF和NHibernate之間的另一個很大的不同點。在NHibernate中,數據庫事務完全掌握在開發者手中。NHibernate的Session永遠不會自動地打開一個顯式數據庫事務。
你可以重寫EF的默認行為並控制數據庫事務范圍和隔離級別
using (var context = new MyDbContext(ConnectionString)) { using (var transaction =context.BeginTransaction(IsolationLevel.RepeatableRead)) { [...] context.SaveChanges(); transaction.Commit(); } }
手動控制數據庫事務范圍的一個非常明顯的副作用就是你必須在整個事務范圍中讓數據庫連接和事務保持打開。
你應當盡可能的讓這個事務范圍生命周期短暫。打開一個數據庫事務運行太長時間可能會對應用程序的性能和可擴展性有非常巨大的影響。特別指出的是,盡量不要再一個顯示事務范圍內調用其它的服務方法——它們可能執行長時間運行的操作而沒有意識到它們是在一個打開的數據庫事務內被調用。
EF沒有內建的方式來重寫用作自動提交事務和自動顯式事務的默認隔離級別
就像上面提到的,EF依賴自動提交事務來執行讀操作並且當調用SaveChanges()方法的時候自動以數據庫配置的默認隔離級別開啟一個顯式事務。
很不幸的是沒有內建的方式來重寫這些隔離級別,如果你想用另一個隔離級別,你必須自己開啟和管理數據庫事務。
通過DbContext打開的數據庫連接自動加入一個周圍環境的TransactionScope
另外,你也可以用TransactionScope來控制事務范圍和隔離級別。EF打開的數據庫連接自動加入周圍環境的TransactionScope。
在EF6之前,使用TransactionScope是唯一可靠的方式來控制數據庫事務范圍和隔離級別。
在實踐中,除非你真的需要一個分布式事務,否則盡量避免使用TransactionScope。TransactionScope,通常指分布式事務,對大部分應用程序來說都是不必要的。並且它們通常會帶來比它們解決的問題都要更多的問題。如果你真的需要一個分布式事務的話,可以查看EF文檔章節——在EF中使用TransactionScope。
DbContext實例應當被釋放掉(但是如果沒有釋放掉,也可能沒事)
DbContext實現了IDisposable接口,因此一旦它們不需要了就應當盡快釋放。
然而在實踐中,除非你選擇顯式控制DbContext使用的數據庫連接或者事務,否則不調用DbContext.Dispose()方法也不會引起任何問題——就像Diego Vega,一個EF團隊成員解釋的那樣。
這是一個好消息——因為你會發現很多代碼不能正確地釋放DbContext實例。尤其是那些嘗試用DI容器來管理DbContext實例生命周期的情況——實際情況比聽起來要棘手得多。
一個DI容器,比如說StructureMap,它不支持釋放它創建的組件。因此,如果你依賴StructureMap來創建DbContext實例,那么它們將不會被釋放掉——不管你為它們設置的什么生命周期方式。使用像這樣的DI容器來管理可釋放組件的唯一正確方式就是復雜你的DI配置並且使用一個嵌套依賴注入容器——就像Jeremy Miller描述的那樣。
