C#实体框架:在存储库类内部正确使用DBContext类


74

我曾经实现我的存储库类,如下所示

public Class MyRepository
{
      private MyDbContext _context; 

      public MyRepository(MyDbContext context)
      {
          _context = context;
      }

      public Entity GetEntity(Guid id)
      {
          return _context.Entities.Find(id);
      }
}

但是,我最近阅读了这篇文章,该文章说将数据上下文作为存储库中的私有成员是一种不好的做法:http : //devproconnections.com/development/solving-net-scalability-problem

现在,从理论上讲,这篇文章是对的:由于DbContext实现IDisposable,因此最正确的实现如下。

public Class MyRepository
{
      public Entity  GetEntity(Guid id)
      {
          using (MyDbContext context = new MyDBContext())
          {
              return context.Entities.Find(id);
          }
      }
}

但是,根据另一篇文章,部署DbContext并不是必需的:http ://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext.html

这两篇文章中哪一篇是对的?我很困惑 如第一篇文章所述,在存储库类中将DbContext作为私有成员真的会导致“可伸缩性问题”吗?


2
我一直都知道DBContext应该只开放给一个工作单元
Mark Homer

那么,您喜欢第二个代码吗?
Errore Fatale 2015年

对我来说看起来更好些
马克·荷马

2
到目前为止,有十个答案。“第一种方法”,“第二种方法”,“另一种方法”。这是一个主要基于意见的问题的教科书示例。如果不是为了赏金,我将投票关闭它。
Gert Arnold

13
@Gert Arnold这是一个基于意见的问题吗?询问使用语言或框架的正确方法是什么,这不是意见。
Errore Fatale

Answers:


42

我认为您不应该关注第一篇文章,我会告诉您原因。

按照第一种方法,您几乎失去了Entity Framework通过提供的所有功能DbContext,包括其一级缓存,其标识映射,其工作单元以及变更跟踪和延迟加载功能。这是因为在上述情况下,DbContext将为每个数据库查询创建一个新实例,并在此后立即进行处置,从而阻止该DbContext实例跟踪整个业务交易中数据对象的状态。

有一个DbContext在你的仓库类作为私有财产也有它的问题。我相信更好的方法是使用CustomDbContextScope。这个人很好地解释了这种方法:Mehdi El Gueddari

本文http://mehdi.me/ambient-dbcontext-in-ef6/是我所见过的有关EntityFramework的最佳文章之一。您应该完整阅读它,我相信它将回答您的所有问题。


+1,您提供的链接描述了一种有趣的方法。它为挑战提供了扎实的解释,并且为解决方案提供了很好的实现。我还没有机会进行测试,但是能够使用嵌套上下文作用域而不需要使用事务作用域的想法很棒。
ken2k 2015年

1
mehdi.me文章中描述的方法做出了许多假设,我认为这些假设对于许多应用程序可能是不正确的(仅允许一次调用SaveChanges,不良的事务处理)。他的建议方法和实现方式有很多好处,但是我们不得不对其进行大量修改。
BenCr '16

好吧,通常一次调用SaveChanges()是使用EF6的正确方法。但是当然,每种情况都是不同的,有时我们可能不得不修改规则。我只是想给出一个更“通用”的答案:)
Fabio Luz

1
如果您要直接复制链接的文章的一部分,错别字和所有内容,则应该在第二
句中加上大括号

19

假设您有多个存储库,并且需要更新来自不同存储库的2条记录。并且您需要进行事务处理(如果一次失败-两次更新都会回滚):

var repositoryA = GetRepository<ClassA>();
var repositoryB = GetRepository<ClassB>();

repository.Update(entityA);
repository.Update(entityB);

因此,如果每个存储库都有自己的DbContext(案例2),则需要使用TransactionScope来实现。

更好的方法-为一个操作(一个调用,一个工作单元)拥有一个共享的DbContext 。因此,DbContext可以管理事务。EF就是这么做的。您只能创建一个DbContext,在许多存储库中进行所有更改,一次调用SaveChanges,在完成所有操作和工作后将其处置。

是UnitOfWork模式实现的示例。

第二种方法可能适合只读操作。


4
这就是为什么存储库是失败概念的典型代表。只需对每个实体的回购说不。
杰夫·邓洛普

9
那个怎么样?UnitOfWork仍然使用每个实体概念的存储库。如果有这些示例,则表明控制器正在做很多工作。应该有一个服务层来完成所有工作,并且UoW应该在服务层内部。控制器应该薄而笨拙。
user441521

13

根本规则是:您的DbContext生存期应限于您正在运行的事务

在此,“交易”可以指只读查询或写查询。正如您可能已经知道的,交易应该尽可能短。

就是说,我要说的是,在大多数情况下,您应该赞成“使用”方式,而不要使用私人成员。

我可以看到使用私有成员的唯一情况是针对CQRS模式(CQRS:如何工作的交叉检验)

顺便说一句,在乔恩·加兰特(Jon Gallant)的帖子中,迭戈·维加(Diego Vega)的回应也给出了一些明智的建议:

我们的示例代码倾向于始终使用“使用”或以其他方式处置上下文的主要原因有两个:

  1. 默认的自动打开/关闭行为相对容易覆盖:您可以通过手动打开连接来控制何时打开和关闭连接。一旦在代码的某些部分开始执行此操作,然后忘记丢弃上下文就变得有害,因为您可能会泄漏打开的连接。

  2. DbContext按照推荐的模式实现IDiposable,该模式包括公开一个虚拟的受保护的Dispose方法,例如,如果需要将其他非托管资源聚合到上下文的生存期中,派生类型可以覆盖该方法。

高温超导


5

使用哪种方法取决于存储库的责任。

存储库有责任运行完整事务吗?即进行更改,然后通过调用SaveChanges将更改保存到数据库中?还是仅是较大事务的一部分,因此仅进行更改而不保存更改?

情况1)存储库将运行完整的事务(它将进行更改并保存):

在这种情况下,第二种方法更好(第二种代码示例的方法)。

我将仅通过引入如下工厂来稍微修改此方法:

public interface IFactory<T>
{
    T Create();
}

public class Repository : IRepository
{
    private IFactory<MyContext> m_Factory;

    public Repository(IFactory<MyContext> factory)
    {
        m_Factory = factory;
    }

    public void AddCustomer(Customer customer)
    {
        using (var context = m_Factory.Create())
        {
            context.Customers.Add(customer);
            context.SaveChanges();
        }            
    }
}

我正在做此微小更改以启用依赖关系注入。这使我们能够在以后更改创建上下文的方式。

我不希望存储库负责创建自己的上下文。实施工厂IFactory<MyContext>将负责创建上下文。

请注意,存储库是如何管理上下文的生存期的,它创建了上下文,进行了一些更改,保存了更改,然后处置了上下文。在这种情况下,存储库的生存期比上下文长。

案例2:存储库是更大事务的一部分(它将进行一些更改,其他存储库将进行其他更改,然后其他人将通过调用来提交事务SaveChanges):

在这种情况下,第一种方法(您先在问题中描述)会更好。

想象一下,继续了解存储库如何成为更大事务的一部分:

using(MyContext context = new MyContext ())
{
    repository1 = new Repository1(context);
    repository1.DoSomething(); //Modify something without saving changes
    repository2 = new Repository2(context);
    repository2.DoSomething(); //Modify something without saving changes

    context.SaveChanges();
}

请注意,每个事务都使用存储库的新实例。这意味着存储库的生存期非常短。

请注意,我正在代码中新建存储库(这违反了依赖注入)。我只是以这个为例。在实际代码中,我们可以使用工厂来解决此问题。

现在,我们可以对该方法进行的一项增强功能是将上下文隐藏在接口后面,以使存储库不再具有访问权限SaveChanges(请参阅“接口隔离原理”)。

您可以有以下内容:

public interface IDatabaseContext
{
    IDbSet<Customer> Customers { get; }
}

public class MyContext : DbContext, IDatabaseContext
{
    public IDbSet<Customer> Customers { get; set; }
}


public class Repository : IRepository
{
    private IDatabaseContext m_Context;

    public Repository(IDatabaseContext context)
    {
        m_Context = context;
    }

    public void AddCustomer(Customer customer)
    {
        m_Context.Customers.Add(customer);      
    }
}

如果需要,可以将所需的其他方法添加到接口。

另请注意,此接口不继承自IDisposable。这意味着Repository该类不负责管理上下文的生存期。在这种情况下,上下文的生存期比存储库更长。其他人将管理上下文的生存期。

关于第一篇文章的注释:

第一篇文章建议您不应使用在问题中描述的第一种方法(将上下文注入存储库中)。

关于如何使用存储库,本文尚不清楚。它是否用作单笔交易的一部分?还是跨多个交易?

我猜(我不确定),在本文所描述的方法中(否定性地),存储库用作长期运行的服务,将涉及许多事务。在这种情况下,我同意该文章。

但是我在这里提出的建议是不同的,我建议仅在每次需要事务时都创建存储库的新实例的情况下使用此方法。

关于第二篇文章的注释:

我认为第二篇文章所讨论的内容与您应该使用哪种方法无关。

第二篇文章讨论了在任何情况下(与存储库的设计无关)是否有必要处置上下文。

请注意,在两种设计方法中,我们正在处理上下文。唯一的区别是谁负责这种处置。

文章说,DbContext似乎可以清理资源而无需显式处理上下文。


IDatabaseContext如果内部存储库不包含任何DbContext方法(例如as),您将如何使用它context.Database.ExecuteSqlCommand
Muflix,

4

您链接的第一篇文章忘记了精确的一件事:所谓的NonScalableUserRepostory实例的生存期是多少(它也忘记了实现NonScalableUserRepostory工具IDisposable,以正确处置该DbContext实例)。

想象以下情况:

public string SomeMethod()
{
    using (var myRepository = new NonScalableUserRepostory(someConfigInstance))
    {
        return myRepository.GetMyString();
    }
}

好吧...类DbContext内仍然会有一些私有字段NonScalableUserRepostory,但是上下文只会使用一次。因此,它与本文描述的最佳实践完全相同。

因此,问题不是“我应该使用私有成员还是using语句? ”,更多的是“上下文的生命周期应该是什么? ”。

答案将是:尝试尽可能缩短它。有工作单位的概念,它代表业务运作。基本上DbContext,每个工作单元都应该有一个新的。

如何定义工作单元以及如何实现它取决于您应用程序的性质;例如,对于ASP.Net MVC应用程序,的生存期DbContext通常是的生存期HttpRequest,即,每次用户生成新的Web请求时都会创建一个新的上下文。


编辑:

要回答您的评论:

一种解决方案是通过构造函数注入工厂方法。这是一些基本示例:

public class MyService : IService
{
    private readonly IRepositoryFactory repositoryFactory;

    public MyService(IRepositoryFactory repositoryFactory)
    {
        this.repositoryFactory = repositoryFactory;
    }

    public void BusinessMethod()
    {
        using (var repo = this.repositoryFactory.Create())
        {
            // ...
        }
    }
}

public interface IRepositoryFactory
{
    IRepository Create();
}

public interface IRepository : IDisposable
{
    //methods
}

但是,这里存在一个问题:考虑到该存储库最有可能被业务层使用,并且考虑到该业务层是您要测试的应用程序的一部分……如何注入假的存储库进行测试,如果实例是在每种方法中创建的,而不是被注入的?
Errore Fatale

@ErroreFatale我添加了一些示例来回答这个问题
ken2k

顺便说一句,给那些反对的人:至少尝试解释原因
ken2k 2015年

谢谢,我将IRepository接口添加到了您的代码段中。请务必注意,IRepository必须是IDisposable。如果有任何错误,您可以纠正它。
Errore Fatale

@ErroreFatale是的,很重要,因为我使用了该using语句,IRepository因此必须是一次性的。感谢您的编辑
ken2k 2015年

2

第一个代码与可伸缩性问题无关,之所以不好,是因为他为每个不好的存储库创建了新的上下文,评论者对此发表了评论,但他甚至没有回答。在Web中,它是1请求1 dbContext,如果您打算使用存储库模式,则它将转换为1请求>许多存储库> 1 dbContext。使用IoC可以轻松实现,但这不是必需的。这是没有IoC的情况下的方法:

var dbContext = new DBContext(); 
var repository = new UserRepository(dbContext); 
var repository2 = new ProductRepository(dbContext);
// do something with repo

至于是否进行处置,我通常会进行处置,但是如果领导本身说了这句话,那么可能就没有理由这样做了。我只想处理是否具有IDisposable。


1

基本上,DbContext类不过是一个包装器,用于处理所有与数据库有关的东西,例如:1.创建连接2.执行查询。现在,如果我们使用普通的ado.net进行上述操作,则需要通过在using语句中编写代码或在连接类对象上调用close()方法来显式地正确关闭连接。

现在,由于上下文类在内部实现了IDisposable接口,因此优良作法是在using语句中编写dbcontext,这样我们就不必关心关闭连接了。

谢谢。


我不同意您的论点(但也会推荐这种using模式)。DbContext还可以处理缓存,更改跟踪等。我不确定,但我认为每个数据库调用都会打开和关闭连接。
xum59

0

我使用第一种方法(注入dbContext),当然它应该是IMyDbContext,并且依赖注入引擎正在管理上下文的生命周期,因此它仅在需要时才处于活动状态。

这使您可以模拟要测试的上下文,第二种方法使您无法在没有数据库供上下文使用的情况下检查实体。


0

第二种方法(使用)更好,因为它肯定只在最短的时间内保持连接,并且更容易实现线程安全。


但这会将UOW扔出窗外!
Seabizkit

是的,但是死锁的机会更少,并且可以将打开的连接数保持在最低水平。理论与实践:)对于获得运营,UOW并不总是很重要。
Dexion

我喜欢这种方法。有人可以解释一下我们在DI时推荐的这种方法吗?
Dilhan Jayathilake

0

我认为最好的方法是,即使您处置它,也不必为每个存储库都创建一个dbcontext。但是,在第一种情况下,可以使用databaseFactory仅实例化一个dbcontext:

 public class DatabaseFactory : Disposable, IDatabaseFactory {
    private XXDbContext dataContext;

    public ISefeViewerDbContext Get() {
        return dataContext ?? (dataContext = new XXDbContext());
    }

    protected override void DisposeCore() {
        if (dataContext != null) {
            dataContext.Dispose();
        }
    }
}

并在存储库中使用此实例:

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
    private IXXDbContext dataContext;

    private readonly DbSet<TEntity> dbset;

    public Repository(IDatabaseFactory databaseFactory) {
        if (databaseFactory == null) {
            throw new ArgumentNullException("databaseFactory", "argument is null");
        }
        DatabaseFactory = databaseFactory;
        dbset = DataContext.Set<TEntity>();
    }

    public ISefeViewerDbContext DataContext {
        get { return (dataContext ?? (dataContext = DatabaseFactory.Get()));
    }

    public virtual TEntity GetById(Guid id){
        return dbset.Find(id);
    }
....
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.