在EntityFramework6中管理DbContext的正確方式(2)【DbContext的預設行為】

風靈使發表於2018-05-28

(譯者注:使用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中最棘手的問題之一就是正確的管理資料庫事務。由於NHibernateSession可以在它的整個生命週期中的任何時間點自動地將未持久化的變化持久化到資料庫,並且可能在一個業務事務裡面持久化多次——這兒沒有一個定義良好的點或者方法來開啟資料庫事務以確保所有的修改以原子的行為提交或者回滾。

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

---------------------------------------------------

-- 隱式提交事務

-- 6SET 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

----------------------------------------------------

-- 顯示事務

-- 6DECLARE @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中,資料庫事務完全掌握在開發者手中。NHibernateSession永遠不會自動地開啟一個顯式資料庫事務。

你可以重寫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是唯一可靠的方式來控制資料庫事務範圍和隔離級別。

在實踐中,除非你真的需要一個分散式事務,否則儘量避免使用TransactionScopeTransactionScope,通常指分散式事務,對大部分應用程式來說都是不必要的。並且它們通常會帶來比它們解決的問題都要更多的問題。如果你真的需要一個分散式事務的話,可以檢視EF文件章節——在EF中使用TransactionScope

DbContext例項應當被釋放掉(但是如果沒有釋放掉,也可能沒事)

DbContext實現了IDisposable介面,因此一旦它們不需要了就應當儘快釋放。

然而在實踐中,除非你選擇顯式控制DbContext使用的資料庫連線或者事務,否則不呼叫DbContext.Dispose()方法也不會引起任何問題——就像Diego Vega,一個EF團隊成員解釋的那樣

這是一個好訊息——因為你會發現很多程式碼不能正確地釋放DbContext例項。尤其是那些嘗試用DI容器來管理DbContext例項生命週期的情況——實際情況比聽起來要棘手得多。

一個DI容器,比如說StructureMap,它不支援釋放它建立的元件。因此,如果你依賴StructureMap來建立DbContext例項,那麼它們將不會被釋放掉——不管你為它們設定的什麼生命週期方式。使用像這樣的DI容器來管理可釋放元件的唯一正確方式就是複雜你的DI配置並且使用一個巢狀依賴注入容器——就像Jeremy Miller描述的那樣。

相關文章