在EntityFramework6中管理DbContext的正確方式(2)【DbContext的預設行為】
(譯者注:使用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描述的那樣。
相關文章
- 在EntityFramework6中管理DbContext的正確方式(3)【環境上下文DbContext vs 顯式DbContext vs 注入DbContext】FrameworkContext
- 在EntityFramework6中管理DbContext的正確方式(4)【DbContextScope:一個簡單的,正確的並且靈活的管理DbContext例項的方式】FrameworkContext
- 在EntityFramework6中管理DbContext的正確方式(1)【考慮的關鍵點】FrameworkContext
- 避免DbContext同時在多個執行緒呼叫Context執行緒
- 避免用using包裝DbContext【翻譯】Context
- Recoil 中預設值的正確處理
- mysqljs在koa2中的正確姿勢MySqlJS
- Protobuf在Cmake中的正確使用
- Entity Framework使用DBContext實現增刪改查示例FrameworkContext
- 在績效管理過程中,管理者如何對員工進行正確的反饋?
- 在iOS中如何正確的實現行間距與行高iOS
- 程式設計師正確看程式碼的方式程式設計師
- OnlineJudge的正確開啟方式
- WikiPedia 的正確開啟方式
- 日期的正確儲存方式
- API 演進的正確方式API
- [譯] 論 Android 中 Span 的正確開啟方式Android
- Node中POST請求的正確處理方式
- yii2-wx / 在yii2-wx中如何正確的使用try….catch….
- webpack4 正確的配置方式Web
- jQuery匯入html的正確方式jQueryHTML
- background-position的正確理解方式
- springboot 中如何正確的在非同步執行緒中使用requestSpring Boot非同步執行緒
- 詳細分析 Java 中啟動執行緒的正確和錯誤方式Java執行緒
- 程式設計師計算私活薪資的正確方式程式設計師
- 在 JavaScript 中建立陣列的正確姿勢JavaScript陣列
- Java學習的正確開啟方式Java
- Laravel Admin 自定義 JavaScript 的正確方式?LaravelJavaScript
- 雲遊戲的正確開啟方式遊戲
- “布”道AI的正確開啟方式AI
- Linux 查詢檔案的正確方式Linux
- 這才是開啟風變程式設計的正確操作方式程式設計
- .NET Core中的鑑權授權正確方式(.NET5)
- 在React ClassComponent中繫結方法的正確姿勢React
- 在Flutter中嵌入Native元件的正確姿勢是...Flutter元件
- 如何進行正確的 CodeReviewView
- 巨省視訊記憶體的重計算技巧在TF、Keras中的正確開啟方式記憶體Keras
- DevOps如何正確的在企業內進行實踐dev