(譯者註:使用EF開發應用程式的一個難點就在於對其DbContext的生命周期管理,你的管理策略是否能很好的支持上層服務 使用獨立事務,使用嵌套事務,並行執行,非同步執行等需求? Mehdi El Gueddari對此做了深入研究和優秀的工作並且寫了一篇優秀的文章,現在我將其翻譯為中文分享給大家。由於 ...
(譯者註:使用EF開發應用程式的一個難點就在於對其DbContext的生命周期管理,你的管理策略是否能很好的支持上層服務 使用獨立事務,使用嵌套事務,並行執行,非同步執行等需求? Mehdi El Gueddari對此做了深入研究和優秀的工作並且寫了一篇優秀的文章,現在我將其翻譯為中文分享給大家。由於原文太長,所以翻譯後的文章將分為四篇。你看到的這篇就是是它的第一篇。原文地址:http://mehdi.me/ambient-dbcontext-in-ef6/)
關於DbContext
這不是第一篇寫關於如何管理基於EntityFramework應用程式的DbContext的生命周期的文章。實際上,這兒已經有非常多的討論這個話題的文章了。
對於許多應用程式,上面這些文章呈現的解決方案(基本上都是利用DI容器對每一個Web請求註入一個DbContext的實例)能夠很好的工作。它們的優點也是非常簡單——至少第一眼看上去是這樣的。
然而,對於某些特定的應用程式,這些解決方案天生的缺陷產生了問題。那就是一些功能變得無法實現或者需要求助於增加複雜的結構或增加醜陋的演算法來處理DbContext的創建和管理。
下麵是一些真實世界的應用程式的示例,它們促使我重新思考管理DbContext的方式:
●一個應用程式包含 ASP.NET MVC和WebAPI構建的Web應用程式。它也可能包含Console和Windows Services構建的後臺服務,包含任務調度服務以及那些處理來自於MSMQ和RabbitMQ的消息的服務。我在上面鏈接的大部分文章都假定所有服務運行在Web請求的上下文裡面,但這裡涉及的情況顯然不是這樣。
●它從多個資料庫讀寫數據,這些資料庫包括一個主資料庫,一個從資料庫,一個報表資料庫和一個日誌資料庫。它的領域模型被分為幾個獨立的組,每一個組有自己的DbContext類型,假定只有一個DbContext類型在這兒無論如何也是行不通的。
●它非常依賴第三方的遠程API,比如Facebook,Twitter或者LinkedIn的API,然而這些API並不支持事務。許多用戶操作要求在返回結果給用戶之前要先進行幾個遠程API調用。我在上面鏈接的大部分文章都假定“一個Web請求只包含一個業務事務”,它應該要麼被提交要麼被回滾,很顯然這條規則在這兒不適用。因為一個遠程API調用失敗並不意味著我們能神奇的“回滾”之前任何一個已經完成的遠程API調用的結果。(例如:當你使用Facebook API向Facebook提交一個狀態更新時,你不能因為後續操作失敗而回滾向Facebook的狀態更新)。因此,在這類應用程式中,一個用戶操作經常要求我們執行多個業務事務,每個事務都能獨立的持久化。(你可能會爭辯說也許有某種方式去重新設計整個系統以避免我們遇到這種情況——當然這是可能的。但如果程式原本就設計成那樣,並且運行得很好而且我們不得不處理這種情況呢?)
●許多服務都嚴重並行化,要麼藉助於非同步IO或者(更常見地)通過TPL庫提供Task.Run()或者Parallel.Invoke()方法將任務分發到多個線程上去執行。在這種場景下,管理DbContext的大部分常見方法都不管用了。
在這篇文章中,我將深入介紹關於DbContext生命周期管理的各個部分。我們將看到解決這個問題的幾種常見策略的優缺點。最後,我們將總結出一個管理DbContext生命周期的策略,它能應對上面提到的各種挑戰。
當然,世上沒有完美的解決方案。但是在文章的最後,你將擁有為你特定的應用程式做出明智決定的工具和知識。
這是一篇非常長而且詳細的文章,它需要一定的時間去閱讀和消化。對於基於EntityFramework的應用程式,你選擇用來管理DbContext生命周期的策略將是一個重要的決定——它將對你的程式的正確性、可維護性、擴展
性產生重大影響。因此,這值得我們多花一些時間認真考慮後再做出選擇。
本文中要用到的術語
在這篇文章中,我將多次提到“服務”這個詞,它不是指的遠程服務(REST或者其它),相反,它是指服務對象——也就是你經常放置業務邏輯實現的地方——這些對象負責執行業務規則和定義業務事務邊界。
當然,你的代碼基可能用的不同的名字,這取決於你創建應用程式架構所使用的設計模式。因此,我所說的“服務”對你來說也可能叫做“工作流(workflow)”,“協調器(orchestrator)”,“執行者(executor)”,“interactor”,“命令(command)”,“處理者(handler)”或者其它一些名字。
更不用說還有很多應用程式根本就沒有定義一個合適的地方來存放業務邏輯,而是隨便放在一個地方,比如說MVC應用程式裡面的控制器(controllers)。
但是這些都和我們討論的話題無關——當我說“服務”的時候,就是指存放業務邏輯的地方,它可以是一個隨便的控制器(controller)方法或者是一個分層架構中的服務類。
考慮的關鍵點
當制定或者評估一個管理DbContext生命周期策略的時候,記住它要支持的關鍵場景和功能是非常重要的。
下麵是一些我認為對大部分程式都很重要的東西。
你的服務必須控制業務服務的邊界(但不是必需控制DbContext的生命周期)
可能管理DbContext的主要難點是理解DbContext的生命周期與業務事務的生命周期這二者之間的差異和關聯。
DbContext實現了工作單元模式:
維護受業務影響的對象列表,並協調變化和併發問題的解決。
在實踐中,當你用DbContext實例去載入,更新,添加和刪除可持久化的實體時,它會在記憶體中跟蹤這些變化。除非你調用SaveChanges()方法,否則它不會將這些變化持久化到底層資料庫。
一個服務方法,就像上面定義的,它將負責定義業務事務的邊界。
這樣的結果就是:
●一個服務方法必須用同一個DbContext實例貫穿整個業務事務,這樣才能跟蹤對可持久化對象的所有修改,並且將這些修改以原子的方式要麼提交到底層資料庫要麼回滾。
●你的服務必須是系統中唯一負責調用DbContext.SaveChanges()方法的組件。如果系統的其它模塊(比如倉儲(repsitory)方法)調用SaveChanges()方法會產生什麼結果呢?它將導致提交部分變化,使你的數據處於一種不一致的狀態。
●SaveChanges()方法必需是在每個業務事務的最後剛好調用一次。如果在業務事務的中間調用也可能會導致不一致的,部分提交的狀態。
一個DbContext實例是可以跨越多個(順序的)業務事務的。一旦一個業務事務已經完成並且調用DbContext.SaveChanges()方法持久化了所有的修改,那麼我們在下一個業務事務中重用同一個DbContext是完全可能的。
也就是說,DbContext實例的生命周期沒有必要和一個單獨的業務事務生命周期綁定在一起。
獨立於業務事務的生命周期來管理DbContext實例的生命周期的優缺點
示例
獨立於業務事務生命周期來管理DbContext實例的生命周期的一個常見場景就是Web應用。常見的處理方式是:DbContext實例在每一個請求開始的時候就被創建,然後在這個Web請求的執行過程中被所有的服務調用,並且在請求結束時被釋放掉。
優點
下麵是關於你為什麼要將DbContex實例的生命周管理從業務事務生命周期管理分離開的兩個重要原因。
●潛在的性能提升。每一個DbContext實例都維護了一個從資料庫載入的對象的一級緩存。當你通過主鍵查詢一個實體時,DbContext將優先從一級緩存獲取它,在獲取不到時,才會嘗試從資料庫查詢。取決於你的數據查詢模式,在多個順序的業務事務中重用同一個DbContext將會因為一級緩存而導致更少的資料庫查詢。
●更多使用延遲載入的場景。如果服務返回可持久化對象(而不是view models或者其它DTOs)那麼你就可以利用這些對象的延遲載入功能,載入這些實體的DbContext實例的生命周期必須超越業務事務的範圍。如果服務在返回實體對象之前就釋放掉了DbContext實例,那麼在這些返回對象上觸發的任何延遲載入屬性都將失敗(是否使用延遲載入功能是另一種爭論,本文不做深入討論)。在我們的Web應用示例中,延遲載入常用於服務層返回到控制器動作方法(controller action method)的實體上。這種情況下,服務方法用來載入實體的DbContext實例的生命周期將在整個Web請求過程中(或者至少持續到動作方法的結束)保持激化狀態。
保持DbContext超越業務事務範圍都處於激活狀態帶來的問題
雖然跨越多個業務事務重用同一個DbContext是可以的,但是DbContext的生命周期還是應該保持得短一些。它的一級緩存最終會過時,並導致併發問題。如果你的應用程式使用樂觀併發策略的話,那麼業務事務將會因為DbUpdateConcurrencyException而失敗。在Web應用中使用“一個web請求一個DbContext實例”這種策略通常是可以的——因為Web請求時間通常很短。但是在桌面應用中,經常被建議使用的策略是使用“一個視窗(form)一個DbContext實例”,但這會經常出現問題——因此在採納前應多考慮。
需要註意的是如果你用悲觀併發策略的話,那麼你不能跨越多個業務事務重用同一個DbContext實例。正確地實現悲觀併發策略牽涉到在整個DbContext實例的生命周期中都要以正確的資料庫隔離級別保持一個激活的資料庫事務——這將防止你在獨立的業務事務中獨立的提交或者回滾數據。
在超過一個業務事務中重用同一個DbContext實例也可能會導致災難性的bug——服務方法可能意外的提交了來自上一個失敗的業務事務的修改。
最後,在你的服務方法的外面來管理DbContext實例的生命周期會傾向於把你的應用程式和一個制定的基礎架構綁定在一起——從長遠來看,這使你的應用程式更加不靈活並且更難演進和維護。
例如,對於一個剛開始很簡單的Web應用程式來說,它依賴於“一個Web請求創建一個DbContext實例”的策略來管理DbContext的生命周期時,這很容易掉進一個圈套——在控制器(controller)或者視圖(view)中使用延遲載入功能或者在服務方法之間傳遞可持久化對象——假定這些場景都會使用同一個DbContext實例。當不可避免地要引入多線程或者轉移這些操作到後臺WindowsService去的時候,這些精心設計的沙堡就崩塌了——因為這兒沒有更多的Web請求來綁定DbContext實例了。
因此,建議避免獨立於業務事務來管理DbContext實例的生命周期。應當在每一個服務方法內部創建它們自己的DbContext實例,併在業務事務結束的時候釋放該實例。
這將防止在服務外面使用延遲載入(也可以讓服務方法返回傳輸對象而非可持久化對象來防止在服務外面使用延遲載入)。另外,最好不要傳遞可持久化對象給服務——因為這些對象沒有依附在服務將要使用的DbContext上。雖然有這麼多限制,但從長遠來看,它將給我們帶來很好的靈活性和可維護性。
你的服務必須控制資料庫事務的範圍和隔離級別
如果你的應用程式使用的資料庫提供的事務支持ACID四個要素(如果你用的是EntityFramework,那麼肯定就是了),那麼你的服務控制資料庫事務的範圍和隔離級別就很有必要了,否則你就不能寫出正確的代碼。
我們將在後面看到,EntityFramework將所有的寫操作用一個顯示資料庫事務打包在一起——預設情況下,將用READ COMMITTED 隔離級別——也就是SQL Server的預設設置——這能適合大部分業務事務。尤其是你依賴於樂觀併發策略去檢測和避免”更新衝突“的情況,更是如此。
無論如何,大部分應用程式仍然需要為某些特定的操作使用其它的隔離級別。
比如,執行報表查詢的場景,你可能會覺得臟讀不是問題從而選擇使用READ UNCOMMITTED隔離級別——這樣可以消除鎖競爭。
並且有的業務規則可能要去使用REPEATABLE READ 或者 SERIALIZABLE 隔離級別(尤其是你的項目使用悲觀併發策略的話)。這些場景就需要服務顯示控制事務範圍了。
管理DbContext的方式應當獨立於系統架構
軟體系統架構和設計模式會隨著時間進化以適應新的業務規則和負載升級。
你肯定不想因為選擇的管理DbContext生命周期的策略綁定在一個特定的架構而限制你對其進行升級。
管理DbContext的方式應當獨立於應用程式類型
雖然現在大部分應用程式都開始於一個Web應用程式,但是你選擇用來管理DbContext生命周期的策略不應當假定你的服務方法只會在基於Web請求的上下文中被調用。一般來說,你的服務層(如果有的話)應當獨立於使用它的應用程式的類型。
在應用程式啟動不久後,你就可能需要創建命令行工具去執行運維任務或者創建Windows服務來處理定時任務或者需要長時間運行的後臺操作。當這種情況發生的時候,你期望能夠為你的控制台應用程式或者Windows服務程式引用你的服務所在的程式集。你肯定不願看到需要完全重構管理DbContext實例的方式後你的服務才能被不通的應用程式類型使用。
管理DbContext的方式應當支持多種DbContext派生類
如果你的應用程式需要訪問多個資料庫(比如報表資料庫,日誌/審計資料庫)或者你將你的領域模型分離成多個聚合組,那麼你就將有多個DbContext派生類。
對於NHibernate用戶來說,這就相當於管理多個SessionFactory實例。
無論你選擇哪種策略都應當能讓服務選擇需要的DbContext類型。
管理DbContext的方式應當能支持EF6提供的非同步工作流
在.NET 4.5中,ADO.NET引入了支持非同步資料庫查詢的功能。隨後非同步支持也被包括在EntityFramework6中——它允許你使用一個完整的非同步工作流來讀寫數據。
無需多說,無論你選擇什麼來管理DbContext,但它必須能很好地與EF非同步功能協調工作。
(譯者註:下一篇,我們將看到DbContext的一些預設行為)