zoukankan      html  css  js  c++  java
  • 探究Entity Framework如何在多个仓储层实例之间工作单元的实现及原理(2018-05-31、2019-08-16修改部分严重错误代码)

    前言

      1、本文的前提条件:EF上下文是线程唯一,EF版本6.1.3。

      2、网上已有相关API的详细介绍,本文更多的是作为我自己的个人学习研究记录。

      3、2018-05-31修改DbSession.cs部分严重错误代码!

      4、2019-08-16 修改DbContextFactory.cs部分严重错误代码!

    疑问

    用反编译工具翻开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;
        }
    }

    在开发当中,我们会遇到上面代码这样的情况:在service层中调用多个repository实例的Insert操作时无法作为同一个工作单元提交。本文要介绍的方法是使用EF自带的开启事务方法 DbContext.Database.BeginTransaction()  。话不多说,贴解决方案代码。

      

    DbContextFactory.cs放在repository层,GetDbContext()用于获取线程唯一的EF上下文。我是用HttpContext.Current.Items[]实现EF上下文的线程唯一,大家也使用IOC容器。(2019-08-16 新增Dispose()方法)

    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;
            }
    
            /// <summary>
            /// 2019-08-16 新增此方法,在Globa.asax中的Application_EndRequest方法中调用此方法,释放EF上下文
            /// 即在请求处理结束时,需要手动释放EF实例,否则会造成内存泄漏
            /// </summary>
            public static void Dispose()
            {
                DbContextBase dbContext = HttpContext.Current.Items["dbContext"] as DbContextBase;
                if (dbContext != null)
                {
                    dbContext.Dispose();
                }
            }
        }

    DbSession.cs同DbContextFactory.cs放在一起,用于向service层提供EF事务的开启、提交功能。(2018-05-31修改了DbSession.cs文件,我之前把transaction.Dispose方法单独拿出来是错误的无法释放事务的,现在改为放入CommitTransaction方法中)

      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; } finally { transaction.Dispose(); } } } }

    使用示例。

        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)
                {
    
                }
            }
        }

    方法的正确性及原理

    在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()操作。

    另外大家需要注意一下,上面ExecuteInTransaction<T>() 流程4中的“不关闭连接”问题。之所以不会关闭,是因为数据库连接是由我们手动 BeginTransaction() 时打开的。这就需要开发人员在提交事务后及时释放掉事务,以关闭数据库连接。即在调用 DbContext.Database.CurrentTransaction.Commit() 后,一定要 Dispose() 一下!!

     实验截图

    下面的代码后和对应在数据库中的事务日志,证实了两个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

  • 相关阅读:
    php目录递归删除
    php嵌套数据
    HTML 标签
    枚举 递归
    传值传址 结构体
    去超市选择要购买的商品 将数组放入集合
    函数
    集合 ArrayList 类
    特殊集合 Stack Queue Hashtable
    二维数组,多维数组
  • 原文地址:https://www.cnblogs.com/xurongjian/p/9102564.html
Copyright © 2011-2022 走看看