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

来源:https://www.cnblogs.com/xurongjian/archive/2018/05/29/9102564.html
-Advertisement-
Play Games

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

  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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 字典的key是一個可哈希的值,字典的value可以是任何值。 keys():返回鍵 values():返回鍵對應的值 items():返回鍵和值 keys遍歷出來 values遍歷出來 items遍歷出來 get():根據key獲取值,key不存在時,可以指定預設值(None) pop() popi ...
  • 本文來告訴大家一個新的技術DirectComposition,在 win7 之後(實際上是 vista),微軟正在考慮一個新的渲染機制 ...
  • 本文告訴大家如何使用 Marshal 做出可以快速釋放記憶體的大數組。 最近在做 3D ,需要不斷申請一段大記憶體數組,然後就釋放他,但是 C# 對於大記憶體不是立刻釋放,所以就存在一定的性能問題。 在博客園看到了一位大神使用 Marshal 做出快速申請的大數組,於是我就學他的方法來弄一個。本文告訴大... ...
  • 如果在 WPF 需要用多進程通信,一個推薦的方法是 WCF ,因為 WCF 是 RPC 計算。先來講下 RPC (Remote Procedure Call) 遠程過程調用,他是通過特定協議,包括 tcp 、http 等對其他進程進行調用的技術 ...
  • 本文實際只是翻譯 .NET Core foundational libraries 官方文檔的編碼風格 ...
  • 上一個游戲已經告訴大家如何寫多個游戲,現在繼續寫這個無聊的游戲 ...
  • 升級至win10 最新版本10.0.17134,遠程桌面連接Window Server時報錯信息如下: 出現身份驗證錯誤,要求的函數不正確,這可能是由於CredSSP加密Oracle修正。 解決方法: 運行 gpedit.msc 本地組策略: 電腦配置>管理模板>系統>憑據分配>加密Oracle修 ...
  • 說起非同步,Thread,Task,async/await,IAsyncResult 必須掌握 1.線程(Thread) 多線程的意義在於一個應用程式中,有多個執行部分可以同時執行;對於比較耗時的操作(例如io,資料庫操作),或者等待響應(如WCF通信)的操作,可以單獨開啟後臺線程來執行,這樣主線程就 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...