前言 1、本文的前提條件:EF上下文是線程唯一,EF版本6.1.3。 2、網上已有相關API的詳細介紹,本文更多的是作為我自己的個人學習研究記錄。 疑問 用反編譯工具翻開DbContext類可以看到EF本身就是一個實現了工作單元的倉儲層,每運行一次DbContext.SaveChanges()便提交 ...
前言
1、本文的前提條件:EF上下文是線程唯一,EF版本6.1.3。
2、網上已有相關API的詳細介紹,本文更多的是作為我自己的個人學習研究記錄。
疑問
用反編譯工具翻開DbContext類可以看到EF本身就是一個實現了工作單元的倉儲層,每運行一次DbContext.SaveChanges()便提交一次工作單元,那麼本文要探究的問題來了:
- 如何在service層調用多個repository實例時實現工作單元?
- 上述方法的正確性及原理是什麼?
service層的工作單元實現
public class UsersService { private BaseRepository<User> userRepositroy = new BaseRepository<User>(); private BaseRepository<Log> logRepositroy = new BaseRepository<Log>(); public UsersService() { } public void DoSomething() { userRepositroy.Insert(new User()); logRepositroy.Insert(new Log()); } } public class BaseRepository<T> where T : class, new() { public DbContextBase DbContext { get; private set; } private readonly DbSet<T> dbSet; public BaseRepository() { DbContext = DbContextFactory.GetDbContext(); dbSet = DbContext.Set<T>(); } public bool Insert(T entity) { dbSet.Add(entity); int result = DbContext.SaveChanges(); return result > 0; } }View Code
在開發當中,我們會遇到上面代碼這樣的情況:在service層中調用多個repository實例的Insert操作時無法作為同一個工作單元提交。本文要介紹的方法是使用EF自帶的開啟事務方法 DbContext.Database.BeginTransaction() 。話不多說,貼解決方案代碼。
DbContextFactory.cs放在repository層,GetDbContext()用於獲取線程唯一的EF上下文。我是用HttpContext.Current.Items[]實現EF上下文的線程唯一,大家也使用IOC容器。
public class DbContextFactory { public static DbContextBase GetDbContext() { DbContextBase dbContext = HttpContext.Current.Items["dbContext"] as DbContextBase; if (dbContext == null) { dbContext = new DbContextBase(); HttpContext.Current.Items["dbContext"] = dbContext; } return dbContext; } }
DbSession.cs同DbContextFactory.cs放在一起,用於向service層提供EF事務的開啟、提交和釋放功能。
public class DbSession {
public static void BeginTransaction(IsolationLevel iolationLevel = IsolationLevel.Unspecified) { DbContextBase dbContext = DbContextFactory.GetDbContext(); DbContextTransaction transaction = dbContext.Database.CurrentTransaction; if (transaction == null) { dbContext.Database.BeginTransaction(iolationLevel); } } public static void CommitTransaction() { DbContextTransaction transaction = DbContextFactory.GetDbContext().Database.CurrentTransaction; if (transaction != null) { try { transaction.Commit(); } catch (Exception) { transaction.Rollback(); throw; } } }
public static void DisposeTransaction() { DbContextTransaction transaction = DbContextFactory.GetDbContext().Database.CurrentTransaction; if (transaction != null) { transaction.Dispose(); } } }
使用示例,最後一定要調用DisposeTransaction()。
public class UsersService { private BaseRepository<User> userRepositroy = new BaseRepository<User>(); private BaseRepository<Log> logRepositroy = new BaseRepository<Log>(); public UsersService(){} public void DoSomething() { try { DbSession.BeginTransaction(); userRepositroy.Insert(new User()); logRepositroy.Insert(new Log()); DbSession.CommitTransaction(); } catch (Exception ex) { } finally { //這句很重要,一定要釋放事務以關閉資料庫連接 DbSession.DisposeTransaction(); } } }
方法的正確性及原理
在service層主動調用 DbContext.Database.BeginTransaction(),這個方法會對EF上下文連接開啟一個事務。OK,那麼問題又來了,SaveChanges()本身也是事務的,BeginTransaction()又開啟的事務,那不就形成嵌套事務了?接下來,讓我們探討一下這個問題。
首先,通過反編譯工具一層層追蹤DbContext.SaveChanges()方法,追蹤到ObjectContext.cs是下麵這樣的。下麵這幾個方法是依次執行的,不過代碼放在頁面上不好閱讀,嫌麻煩的話可以直接看我接下來對最後一個方法的分析。
public virtual int SaveChanges() { return this.SaveChanges(SaveOptions.AcceptAllChangesAfterSave | SaveOptions.DetectChangesBeforeSave); } public virtual int SaveChanges(SaveOptions options) { return this.SaveChangesInternal(options, false); } internal int SaveChangesInternal(SaveOptions options, bool executeInExistingTransaction) { this.AsyncMonitor.EnsureNotEntered(); this.PrepareToSaveChanges(options); int num = 0; if (this.ObjectStateManager.HasChanges()) { if (executeInExistingTransaction) { num = this.SaveChangesToStore(options, (IDbExecutionStrategy) null, false); } else { IDbExecutionStrategy executionStrategy = DbProviderServices.GetExecutionStrategy(this.Connection, this.MetadataWorkspace); num = executionStrategy.Execute<int>((Func<int>) (() => this.SaveChangesToStore(options, executionStrategy, true))); } } return num; } private int SaveChangesToStore(SaveOptions options, IDbExecutionStrategy executionStrategy, bool startLocalTransaction) { this._adapter.AcceptChangesDuringUpdate = false; this._adapter.Connection = this.Connection; this._adapter.CommandTimeout = this.CommandTimeout; int num = this.ExecuteInTransaction<int>((Func<int>) (() => this._adapter.Update()), executionStrategy, startLocalTransaction, true); if ((SaveOptions.AcceptAllChangesAfterSave & options) != SaveOptions.None) { try { this.AcceptAllChanges(); } catch (Exception ex) { throw new InvalidOperationException(Strings.ObjectContext_AcceptAllChangesFailure((object) ex.Message), ex); } } return num; } internal virtual T ExecuteInTransaction<T>(Func<T> func, IDbExecutionStrategy executionStrategy, bool startLocalTransaction, bool releaseConnectionOnSuccess) { this.EnsureConnection(startLocalTransaction); bool flag = false; EntityConnection connection = (EntityConnection) this.Connection; if (connection.CurrentTransaction == null && !connection.EnlistedInUserTransaction && this._lastTransaction == (Transaction) null) flag = startLocalTransaction; else if (executionStrategy != null && executionStrategy.RetriesOnFailure) throw new InvalidOperationException(Strings.ExecutionStrategy_ExistingTransaction((object) executionStrategy.GetType().Name)); DbTransaction dbTransaction = (DbTransaction) null; try { if (flag) dbTransaction = (DbTransaction) connection.BeginTransaction(); T obj = func(); if (dbTransaction != null) dbTransaction.Commit(); if (releaseConnectionOnSuccess) this.ReleaseConnection(); return obj; } catch (Exception ex) { this.ReleaseConnection(); throw; } finally { if (dbTransaction != null) dbTransaction.Dispose(); } }
由上向下解讀,運行到最後一個方法 ExecuteInTransaction<T>() 時 startLocalTransaction 參數總是為 true,那麼這個方法的簡要流程解讀如下:
- 確保上下文連接Connection處於 opened 狀態;
- flag 值設為 false;
- connection.CurrentTransaction 等於 null,那麼 flag值 設為 true,開啟新事務,執行委托,提交事務,關閉連接,釋放事務;
- connection.CurrentTransaction 不等於 null,那麼 flag值 仍保持為 false,不開啟事務,執行委托,不提交事務,不關閉連接,不釋放事務
接著,摸清上方代碼中的 ObjectContext.connection.CurrentTransaction 與 DbContext.Database.CurrentTransaction 的關係,我們就解決剛纔的問題了:“是不是嵌套事務?”。通過反編譯查看 DbContext.Database 的代碼圖下圖所示(其實,github有EF的源碼可以下載)。是不是發現它們其實就是同一個家伙,後者其實就是披了件馬甲!
最後,到這裡可以清楚的得到這麼個結論:當我們直接調用DbContext.SaveChanges()時,EF會在底層為我們開啟事務並提交;而當我們手動使用 DbContext.Database.BeginTransaction() 開啟事務時,EF則會在我們手動提交事務前合併所有的SaveChanges()操作。
另外大家需要註意一下,在EF6.1.3版本中,上面ExecuteInTransaction<T>() 流程4中的“不關閉連接”問題。之所以不會關閉,是因為資料庫連接是由我們手動 BeginTransaction() 時打開的。這就需要開發人員在提交事務後及時釋放掉事務,以關閉資料庫連接。即在調用 DbContext.Database.CurrentTransaction.Commit() 後,一定要再 Dispose() 一下!!
在EF6.2.0版本中似乎存在部分差異,Commit() 之後事務就被自動釋放掉了。這個後面我做了調查試驗再補充吧。
實驗截圖
下麵的代碼後和對應在資料庫中的事務日誌,證實了兩個Insert操作確實是在同一個事務里的。
參考引用
EF上下文對象線程內唯一性與優化 :https://blog.csdn.net/qq_29227939/article/details/51713422
瞭解Entity Framework中事務處理: https://www.cnblogs.com/from1991/p/5423120.html
如何讀懂SQL Server的事務日誌: https://www.cnblogs.com/Cookies-Tang/p/3750562.html