(譯者註:使用EF開發應用程式的一個難點就在於對其DbContext的生命周期管理,你的管理策略是否能很好的支持上層服務 使用獨立事務,使用嵌套事務,並行執行,非同步執行等需求? Mehdi El Gueddari對此做了深入研究和優秀的工作並且寫了一篇優秀的文章,現在我將其翻譯為中文分享給大家。由於 ...
(譯者註:使用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描述的那樣。