前言

  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. Maximum Subarray LT53

    Given an integer array nums, find the contiguous subarray (containing at least one number) which has ...

  2. APICloud开发

    2018-06-16 今天在看房角石APPIOS版本闪退的问题,后来定位到了 elements.find("video").attr("preload", &q ...

  3. from __future__ import

    读代码的过程中看到的,好奇搜索了一下,其实当在我们调试别人Python代码的过程中经常会遇到一些问题,比如版本不同,代码也会有所改变,比如print函数 Python 2.7版本为 print “ ” ...

  4. java常用设计模式十二:命令模式

    一.概述 定义:命令(Command)模式又叫作动作(Action)模式或事务(Transaction)模式,是一种对象的行为模式.将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化:对 ...

  5. 下载编译安装Apache HTTP Server 2.4.23以及配置HTTP/HTTPS反向代理

    http://blog.csdn.net/gangchengzhong/article/details/52910225 [注意,在编译make时出现的错误并不是文章中说的openssl的版本问题,而 ...

  6. ThinkPHP5 <= 5.0.22 远程代码执行高危漏洞

    漏洞描述 由于ThinkPHP5框架对控制器名没有进行足够的安全检测,导致在没有开启强制路由的情况下,黑客构造特定的请求,可直接GetWebShell. 漏洞评级 严重 影响版本 ThinkPHP 5 ...

  7. HDU 1003 MAXSUM(最大子序列和)

    Description Given a sequence a[1],a[2],a[3]......a[n], your job is to calculate the max sum of a sub ...

  8. 1.7.8使用return 停止线程

    package com.cky.thread; /** * Created by edison on 2017/12/3. */ public class MyThread12 extends Thr ...

  9. 读书笔记 Bioinformatics Algorithms Chapter5

    Chapter5  HOW DO WE COMPARE DNA SEQUENCES  Bioinformatics Algorithms-An_Active Learning Approach htt ...

  10. property属性[Python]

    一.property解释 根据文档资料解释: property([fget[, fset[, fdel[, doc]]]]) Return a property attribute for new-s ...