1. 程式人生 > >探究Entity Framework如何在多個倉儲層實例之間實現工作單元的實現及原理

探究Entity Framework如何在多個倉儲層實例之間實現工作單元的實現及原理

事務日誌 方法 tran action opera and 底層 下載 none

前言

  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,那麽這個方法的簡要流程解讀如下:

  1. 確保上下文連接Connection處於 opened 狀態;
  2. flag 值設為 false;
  3. connection.CurrentTransaction 等於 null,那麽 flag值 設為 true,開啟新事務,執行委托,提交事務,關閉連接,釋放事務;
  4. 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

探究Entity Framework如何在多個倉儲層實例之間實現工作單元的實現及原理