探究Entity Framework如何在多个仓储层实例之间实现工作单元的实现及原理

内容预览:
  • 前言   1、本文的前提条件:EF上下文是线程唯一,EF版本6.1.3~
  • OK,那么问题又来了,SaveChanges()本身也是事务的,BeginTransaction()...~
  • 通过反编译查看 DbContext.Database 的代码图下图所示(其实,github有E...~

前言

  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

 

和大佬一起学习网络安全知识

以上就是:探究Entity Framework如何在多个仓储层实例之间实现工作单元的实现及原理 的全部内容

本站部分内容来源于互联网和用户投稿,如有侵权请联系我们删除,谢谢^^
Email:[email protected]


0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论