繁体   English   中英

c# 实体框架:在您的存储库 class 中正确使用 DBContext class

[英]c# entity framework: correct use of DBContext class inside your repository class

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

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 作为存储库中的私有成员 class 真的会像第一篇文章所暗示的那样导致“可伸缩性问题”吗?

我想你不应该听完第一篇文章,我会告诉你原因。

在第一种方法之后,您几乎失去了Entity Framework通过DbContext提供的所有功能,包括其第一级缓存,其身份映射,工作单元以及其更改跟踪和延迟加载功能。 这是因为在上面的场景中,为每个数据库查询创建了一个新的DbContext实例,并在之后立即处理,从而阻止DbContext实例在整个业务事务中跟踪数据对象的状态。

DbContext作为存储库类中的私有属性也存在问题。 我相信更好的方法是使用CustomDbContextScope。 这个方法很好地解释了这个人:Mehdi El Gueddari

这篇文章http://mehdi.me/ambient-dbcontext-in-ef6/我见过的关于EntityFramework的最好的文章之一。 你应该完全阅读它,我相信它会回答你所有的问题。

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

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

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

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

更好的方法 - 为一个操作设置一个共享DbContext(一个调用,一个工作单元 )。 所以,DbContext可以管理事务。 EF非常适合这种情况。 您只能创建一个DbContext,在许多存储库中进行所有更改,调用SaveChanges一次,在完成所有操作和工作后进行处理。

以下是UnitOfWork模式实现的示例。

您的第二种方式可能适用于只读操作。

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

这里,“事务”可以指只读查询或写查询。 正如您可能已经知道的那样,交易应该尽可能短。

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

我可以看到使用私人会员的唯一一个案例是CQRS模式(CQRS:如何运作的交叉检查)

顺便说一句,在Jon Gallant的帖子中 ,Diego Vega的反应也给出了一些明智的建议:

我们的示例代码总是使用“使用”或以其他方式处理上下文有两个主要原因:

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

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

HTH

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

存储库是否有责任运行完整的事务? 即通过调用`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似乎清理资源而不需要明确地处理上下文。

你链接的第一篇文章忘记了一个重要的事情:所谓的NonScalableUserRepostory实例的生命周期是什么(它也忘了让NonScalableUserRepostory实现IDisposable ,以便正确处理DbContext实例)。

想象一下以下情况:

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

嗯...... NonScalableUserRepostory类中仍然会有一些私有的DbContext字段,但上下文只会使用一次 所以它与文章描述的最佳实践完全相同。

所以问题不是“ 我应该使用私有成员还是使用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
}

第一个代码与scability问题没有关系,它之所以糟糕是因为他为每个存储库创建了新的上下文,这是一个很糟糕的评论者,但是他甚至没有回复。 在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,我只想处理。

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

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

谢谢。

我使用第一种方式(注入dbContext)当然它应该是一个IMyDbContext,并且您的依赖注入引擎正在管理上下文的生命周期,因此它只在需要时才生效。

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

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

我认为第一种方法更好,你永远不必为每个存储库创建一个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();
        }
    }
}

并在Repository中使用此实例:

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);
    }
....

我个人会遵循Microsoft Entity Framework 文档 似乎推荐的技术取决于您的应用程序,但我相信您的第二个代码块更类似于 DbContext 的推荐用法。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM