探究Entity Framework如何在多个仓储层实例之间工作单元的实现及原理(2018-05-31修改部分严重错误代码)
前言
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,那么这个方法的简要流程解读如下:
- 确保上下文连接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()操作。
另外大家需要注意一下,上面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修改部分严重错误代码)的更多相关文章
- UWP开发之ORM实践:如何使用Entity Framework Core做SQLite数据持久层?
选择SQLite的理由 在做UWP开发的时候我们首选的本地数据库一般都是Sqlite,我以前也不知道为啥?后来仔细研究了一下也是有原因的: 1,微软做的UWP应用大部分也是用Sqlite.或者说是微软 ...
- 集成 Entity Framework
ABP 基础设施层——集成 Entity Framework 本文翻译自ABP的官方教程<EntityFramework Integration>,地址为:http://aspnetboi ...
- ABP 基础设施层——集成 Entity Framework
本文翻译自ABP的官方教程<EntityFramework Integration>,地址为:http://aspnetboilerplate.com/Pages/Documents/En ...
- ASP.NET Core 中的 ORM 之 Entity Framework
目录 EF Core 简介 使用 EF Core(Code First) EF Core 中的一些常用知识点 实体建模 实体关系 种子数据 并发管理 执行 SQL 语句和存储过程 延迟加载和预先加载 ...
- Entity Framework Code First实体对象变动跟踪
Entity Framework Code First通过DbContext.ChangeTracker对实体对象的变动进行跟踪,实现跟踪的方式有两种:变动跟踪快照和变动跟踪代理. 变动跟踪快照:前面 ...
- Entity Framework 6.x Code Frist For Oracle 实践与注意点
Entity Framework 6.x Code Frist For Oracle 实践与注意点 开发环境 Visual Studio.net 2015/2017 Oracle 11g/12c 数据 ...
- Entity Framework 系统约定配置
前言 Code First之所以能够让开发人员以一种更加高效.灵活的方式进行数据操作有一个重要的原因在于它的约定配置.现在软件开发越来越复杂,大家都试图将软件设计的越来越灵活,很多内容我们都希望是可配 ...
- 使用工具追踪Entity Framework生成的SQL
学习entity framework期间收集的文章,转自http://www.cnblogs.com/hiteddy/archive/2011/10/01/Difference_among_IQuer ...
- Entity Framework 全面教程详解(转)
目录 预备知识 2 LINQ技术 2 LINQ技术的基础 - C#3.0 2 自动属性 2 隐式类型 2 对象初始化器与集合初始化器 3 匿名类 3 扩展方法 ...
随机推荐
- 树状数组训练题2:SuperBrother打鼹鼠(vijos1512)
先给题目链接:打鼹鼠 这道题怎么写? 很明显是树状数组. 而且,很明显是二维树状数组. 如果你没学过二维的树状数组,那么戳开这里:二维树状数组 看完以后,你就会知道怎么做了. 没有什么好解释的,几乎就 ...
- NOIP2016原题终结测试(2017081801)
NOIP2016还有几道原题没有写掉,今天就一并布置掉. 答案的问题,有部分会先放到NOIP题解中,是单独发布的. 最后会汇总放在答案中,各位不要急. 还有,后期会有原创题测试,这个不急,反正11月才 ...
- python学习 day09 (3月14日)----函数
一.函数的进阶 1.1 动态参数 1.2* ** 1.3*args , **kwargs 1.4 函数的注释 1.5名称空间 1.6函数的嵌套全局变量 : 贴边写的局部变量 : 不是贴边写的. ''' ...
- python学习 day2 (3月2日)
.if if else 和 if elif else 的区别是: 前者 判断第一个 判断完第二个 之后还会执行else: 后者是只有满足条件(即都不符合if.elif里的条件时才会进入else) 不清 ...
- word中括号中公式对齐
在写论文中使用到括号,但没有对齐,如: 但是我需要对齐,如: 此时只需要在每行需要对齐的地方输入‘&’即可(此符号在专业型括号是不可见的,在线性中可见,上图为专业型),同样也可以有多个对齐点, ...
- fmt标签如何计算两个日期之间相隔的天数
<h2>start -- ${start}</h2><h2>end -- ${end}</h2><fmt:formatDate var=" ...
- 2018.11.05 bzoj2143: 飞飞侠(最短路)
传送门 最短路好题. 考虑对每个二维坐标建立一个高度属性. 这样每次如果在点(i,j,0)(i,j,0)(i,j,0)只能选择花费bi,jb_{i,j}bi,j跳向(i,j,ai,j)(i,j,a_ ...
- 应用整合CAS服务器方法
概要 在开发WEB程序时需要整合CAS实现单点登录,下面介绍一下应用整合CAS服务器的过程. 在开始之前,我们确定CAS服务器已经搭建完毕. 实现步骤 1.新建一个maven项目,引入casclien ...
- CAS 单点登录 服务器整合
概述 现在企业内部的系统越来越多,如果各个应用都有自己的用户系统,那么用户将不得不要记住不同系统的用户名密码,因此独立的用户系统应运而生,各个系统之间通过单点登录的方式,这样内部只需要记住一个用户名和 ...
- Maven进行install的时候报错,COMPILATION ERROR : Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.13:test (default-test) on project cmu: There are test failures.
maven进行install的时候,test类里面报错: COMPILATION ERROR : [INFO] -------------------------------------------- ...