一个DbContext实例可以跨越多个仓储库

3
这是我在Web Api中设置的AutoFac-DI定义:

我的AutoFac-DI定义如下:

 builder.RegisterType<MyContext>().As<MyContext>().InstancePerRequest();
 builder.RegisterType<TestRepository>().InstancePerRequest();
 builder.RegisterType<SchoolclassCodeRepository>().InstancePerRequest();
 builder.RegisterType<TestService>().InstancePerRequest();

TestService构造函数接受TestRepository和SchoolclassCodeRepository。这两个存储库都接受MyContext的相同实例。

我同意这个观点:是否明智地使用具有多个存储库的同一DbContext?

然而,共享上下文还有其他很好的理由之一(在我看来),那就是上下文必须跟踪实体的状态,如果您获取一个实体,释放上下文,对实体进行一些修改,然后将其附加到新的上下文中,那么这个新的上下文必须去访问数据库以便确定实体的状态。同样,如果您使用实体图(发票及其所有发票项目)工作,则新上下文必须获取图中的所有实体以确定它们的状态。

但是,现在我在这种架构中遇到了单行道!

如果我需要执行跨多个存储库的事务怎么办?

使用EF6而不使用存储库可以这样做:

using(NorthwindEntities db = new NorthwindEntities())
{
    DbContextTransaction transaction = db.Database.BeginTransaction();

    try
    {
        //insert record 1
        Customer obj1 = new Customer();
        obj1.CustomerID = "ABCDE";
        db.Customers.Add(obj1);
        db.SaveChanges();

        //insert record 2
        Customer obj2 = new Customer();
        obj2.CustomerID = "PQRST";
        db.Customers.Add(obj2);   
        db.SaveChanges();

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
    }
}

现在,当我尝试使用我的服务中的两个仓库进行相同操作时,我遇到了一个严重的问题。

  • 我的服务中没有可用的DbContext。
  • DbContext是一个DataProvider/Layer层的关注点,应该保留在仓库内部。

那么,我如何在不改变我的仓库的情况下跨多个仓库创建事务呢?

以下是我想要的示例:

在我的TestService中,我想要大致执行以下操作:

public void Save()
{
  // Open Transaction
  // testRepo.Insert();
  // schoolclassCodeRepo.Delete();
  // Commit Transaction
}

更新

在我的TestService中,我将所有实体从仓库映射到DTO对象中,然后在我的Web API控制器中通过数据+链接(Rest)进行增强。

更新2

  • 仓储模式可以使数据访问方法可重复使用,这是好的。
  • 但是,它使得对多个共享同一DbContext的仓库进行事务处理不可能。

如果将所有Repository方法实现为DbContext的扩展方法,那么我是否可以直接在注入到我的TestService中的一个DbContext上调用“Repo扩展方法”?

更新3 解决方案代码

public async Task<bool> DeleteSchoolyearAsync(int id)
{
    using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        try
        {
            await testRepository.DeleteTestTypes(id);
            await schoolyearRepository.DeleteAsync(id);
            scope.Complete(); // Rollback is done due to using statement...
            return true;

        }
        catch (System.Exception)
        {
            return false;
        }
    }
}

这段代码运行正常!

“然而,共享上下文的好处有很多,其中之一(在我看来)是错误的。在这种情况下,您刚刚创建了一个内存数据库。由于静态DbContext的项目永远不会被收集,因此缓存中的任何内容都不会被回收。因此,随着时间的推移,更多的数据库会被“缓存”,直到您用尽内存并崩溃或整个数据库都在缓存中。您还失去了线程安全性,因此需要在单线程中运行,因为缓存不是线程安全的。此外,.SaveChanges()会在其他线程中引起副作用。” - Aron
然后再加上一个事实,即SaveChanges()现在必须在整个缓存中运行ChangeTracker,而这个缓存现在是整个数据库,每次调用.SaveChanges()将持续...哦,我不知道...大约5分钟...除非它不会这样做,因为当另一个线程访问DbContext时,它将崩溃。 - Aron
1
@Eldho 那我就需要将 TestRepository 与 SchoolclassCodeRepository 结合起来。这会使每个 Repo 的职责模糊,而我不喜欢这种情况。此外,我希望 testRepo.Insert 成为可重用的方法,但如果我把 schoolclassCode.Delete 放在其中,那么它就无法做到这一点... - Elisabeth
@Jure 没有ChangeTracker会跟踪任何东西。它无法跟踪任何东西,因为对象很可能都是POCO。相反,它存储了对象映射到的所有“单元格”的快照,然后每次调用.SaveChanges()之类的方法时,它会扫描所有实体并将实体与“单元格”进行比较。这非常低效。我的观点是,ChangeTracker会将其加载的所有内容保存在内存中,因此随着时间的推移,服务将把整个数据库加载到“缓存”中,因为释放内存的机制是处理DbContext。 - Aron
@Aron:我之前不知道EF中有两种机制可以执行更改跟踪:基于快照和基于代理。我猜测当属性被标记为虚拟时,会使用基于代理的更改跟踪。 - Jure
显示剩余6条评论
2个回答

3

您不需要更改您的代码库,但是您的架构中肯定缺少工作单元(Unit of Work)。这是在多个代码库之间共享单个上下文的地方。

将UoW视为DbContext,其中代码库是DbSet。

UoW uow = new UoW( context );

uow.BeginTransaction();

uow.Repository1.... // query, insert, update, delete
uow.Repository2....

uow.Commit();

一个典型的实现只是公开多个存储库:

public class UoW {

   public UoW( DbContext ctx ) {

      this._ctx = ctx;

   }

   private Repository1 _repo1;

   public Repository1 Repo1
   {
      get
      {
          if ( _repo1 == null )
              _repo1 = new Repository1( this._ctx );
          return _repo1;
      }

      ...

如果您需要一份良好且全面的教程,可以看看这里:

http://www.asp.net/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application


似乎又是对我的存储库几乎不需要的抽象。与我的实现唯一的区别是每个存储库的SaveChanges()现在移到了UoW类中。这通常是这样的,但在你的解决方案中不是 - 我猜三个点意味着相同......- 为什么不采用用户Shiraz的解决方案呢? - Elisabeth
1
不需要吗?您可能是那些认为存储库不必要的人之一,因为DbContext已经很好地烘焙了。在这里查看我的解释http://stackoverflow.com/a/19059282/941240。两种方法都是有效的-您可以仅使用DbContext,也可以在数据访问上拥有存储库/ uow。关键是:仅具有存储库而没有uow意味着您会遇到问题,从您的问题开始,即不清楚将事务的责任放在哪里。 - Wiktor Zychla
我同意你的观点,@WiktorZychla。在设计时,我意识到仓储不应该控制何时保存数据,因为不同的仓储可能相互依赖,我们可能需要将它们作为单个事务提交。但是我没有使用UoW。我只是在服务消费者(例如控制器)中调用SaveChanges()方法来保存数据。如果实现一个UoW类,我能得到什么好处呢?我认为DbContext已经被构建成一个UoW类了。 - Ghasan غسان
1
@Ghasan:是的,没错。如果您不打算切换到其他数据访问技术,那么坚持使用db context是一个不错的选择。 - Wiktor Zychla

1
您可以使用事务范围来代替 DbContextTransaction 来实现此操作:
using (var scope = new TransactionScope(TransactionScopeOption.Required,
                                            new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
    {

  // Your code

 scope.Complete();

}

注意,当您在不同数据库间使用此功能时,会使用MSDTC。


因为你的“注释”。跨数据库是指当我有多个打开的连接和不同的连接时,对吗? - Elisabeth
我按照你说的做法使用了TransactionScope而不是额外的UnitOfWork类。然后我使用包含TransactionScope的服务进行了正面和负面的集成测试,测试结果良好。我已经更新了我的问题并附上了最终代码! - Elisabeth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接