前言

  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 > ;
}
}

在开发当中,我们会遇到上面代码这样的情况:在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 = ;
  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

探究Entity Framework如何在多个仓储层实例之间工作单元的实现及原理(2018-05-31修改部分严重错误代码)的更多相关文章

  1. UWP开发之ORM实践:如何使用Entity Framework Core做SQLite数据持久层?

    选择SQLite的理由 在做UWP开发的时候我们首选的本地数据库一般都是Sqlite,我以前也不知道为啥?后来仔细研究了一下也是有原因的: 1,微软做的UWP应用大部分也是用Sqlite.或者说是微软 ...

  2. 集成 Entity Framework

    ABP 基础设施层——集成 Entity Framework 本文翻译自ABP的官方教程<EntityFramework Integration>,地址为:http://aspnetboi ...

  3. ABP 基础设施层——集成 Entity Framework

    本文翻译自ABP的官方教程<EntityFramework Integration>,地址为:http://aspnetboilerplate.com/Pages/Documents/En ...

  4. ASP.NET Core 中的 ORM 之 Entity Framework

    目录 EF Core 简介 使用 EF Core(Code First) EF Core 中的一些常用知识点 实体建模 实体关系 种子数据 并发管理 执行 SQL 语句和存储过程 延迟加载和预先加载 ...

  5. Entity Framework Code First实体对象变动跟踪

    Entity Framework Code First通过DbContext.ChangeTracker对实体对象的变动进行跟踪,实现跟踪的方式有两种:变动跟踪快照和变动跟踪代理. 变动跟踪快照:前面 ...

  6. Entity Framework 6.x Code Frist For Oracle 实践与注意点

    Entity Framework 6.x Code Frist For Oracle 实践与注意点 开发环境 Visual Studio.net 2015/2017 Oracle 11g/12c 数据 ...

  7. Entity Framework 系统约定配置

    前言 Code First之所以能够让开发人员以一种更加高效.灵活的方式进行数据操作有一个重要的原因在于它的约定配置.现在软件开发越来越复杂,大家都试图将软件设计的越来越灵活,很多内容我们都希望是可配 ...

  8. 使用工具追踪Entity Framework生成的SQL

    学习entity framework期间收集的文章,转自http://www.cnblogs.com/hiteddy/archive/2011/10/01/Difference_among_IQuer ...

  9. Entity Framework 全面教程详解(转)

    目录 预备知识    2 LINQ技术 2 LINQ技术的基础 - C#3.0    2 自动属性    2 隐式类型    2 对象初始化器与集合初始化器    3 匿名类    3 扩展方法    ...

随机推荐

  1. Java SE学习【二】——面向对象

    面向对象的学习也进行了一段时间,这段时间学了,类和对象:属性:方法:封装:继承:多态:接口.也算是有一些自己的理解,不愧是贴近人类思维的思想,老师讲时我常常会想到以前的一些事物和其交相印证,其中最常想 ...

  2. pygame小记

    pygame.display.set_mode(x, y)设置显示窗口大小pygame.sprite.Sprite方法中有image, rect, speed等参数 其中image 可以通过 pyga ...

  3. Hbase常用操作(增删改查)

    Hbase常用操作(增删改查) [日期:2014-01-03] 来源:Linux社区  作者:net19880504 [字体:大 中 小]     运行Eclipse,创建一个新的Java工程“HBa ...

  4. spring学习 十 schema-based 异常通知,和环绕通知

    一 schema-based异常通知 第一步:创建通知类 :新建一个类实现 throwsAdvice 接口,throwsAdvice接口只是标记接口里面并没有任何方法,必须自己写方法,且必须叫 aft ...

  5. 车站(NOIP1998)

    题目链接:车站 这一题,首先你要会推导,推到出式子后,就会像我一样简单AC. 给一张图: 这里,t是第二个车站上车人数. 有什么规律? 其实很好找.有如下规律: 第x车站的人数增量为第x-2车站的上车 ...

  6. 494. Target Sum - Unsolved

    https://leetcode.com/problems/target-sum/#/description You are given a list of non-negative integers ...

  7. Hadoop3集群搭建之——hive安装

    Hadoop3集群搭建之——虚拟机安装 Hadoop3集群搭建之——安装hadoop,配置环境 Hadoop3集群搭建之——配置ntp服务 Hadoop3集群搭建之——hbase安装及简单操作 现在到 ...

  8. 733. Flood Fill

    class Solution { public: int szx,szy; vector<vector<int>> floodFill(vector<vector< ...

  9. nginx 502 Bad Gateway 错误解决办法

    nginx出现502有很多原因,但大部分原因可以归结为资源数量不够用,也就是说后端PHP-fpm处理有问题,nginx将正确的客户端请求发给了后端的php-fpm进程,但是因为php-fpm进程的问题 ...

  10. MySQL处理表字段小技巧

    MySQL利用正则函数替换值 update dateTest set date=REPLACE(date,'/','') where date REGEXP '\/'; SQL语句讲解: -- 将 所 ...