为什么要使用仓储模式或者请解释一下仓储模式?

66
我正在学习仓储模式,并阅读了《使用Entity Framework 4.1和Code First实现仓储模式》《通用仓储模式——Entity Framework,ASP.NET MVC和单元测试之间的关系》这两篇文章,介绍了如何使用Entity Framework实现仓储模式。
其中提到:

•向上层隐藏EF
•提高代码可测试性

我理解提高代码可测试性,但为什么要向上层隐藏EF呢?

看他们的实现,似乎只是用一个通用的方法来查询Entity Framework。这样做的原因是什么呢?

我猜是为了:

  1. 松耦合(这就是为什么要隐藏EF吗?)
  2. 避免多次编写相同的LINQ语句以进行同一查询

我的理解正确吗?

如果我编写一个DataAccessLayer类,其中包含方法

QueryFooObject(int id)
{
..//query foo from entity framework
} 

AddFooObject(Foo obj)
{
.. //add foo to entity framework
}
......
QueryBarObject(int id)
{
..
}

AddBarObject(Bar obj)
{
...
}

那也是一种仓储模式吗?

对于新手的解释会很好 :)


隐藏EF层,让应用程序与数据层解耦。应用程序不需要知道数据是如何处理的(ADO、EF、Web API或仅用于单元测试的模拟数据)。在运行时,应用程序从配置中注入“a”数据存储库到其控制器中。因此,更换数据层就像更改应用程序的配置一样容易。我将存储库保存在一个单独的项目中,这使得UI项目轻量化且数据无关。 - Denny Jacob
8个回答

88

我认为你不应该这样做。

实体框架已经是对数据库的抽象层。上下文使用工作单元模式,每个DBSet都是一个仓库。在此基础上添加一个仓库模式会使你与ORM的特性相距甚远。

我在我的博客文章中谈到过这个问题: http://www.nogginbox.co.uk/blog/do-we-need-the-repository-pattern

添加自己的仓库实现的主要原因是可以使用依赖注入并使你的代码更具可测试性。

EF本身不太容易进行测试,但很容易制作一个可模拟的EF数据上下文版本,并引入一个可注入的接口。

我在这里谈到了这个问题: http://www.nogginbox.co.uk/blog/mocking-entity-framework-data-context

如果我们不需要仓库模式来使EF具有可测试性,那么我认为我们根本不需要它。


9
我很喜欢你博客文章中的这句话:“这种抽象层可以将ORM的特性与你隔离开来”。有人可能会说,这种“隔离”是存储库的目的。但对于这里许多有关repo + EF的问题,我感觉他们在不充分了解具体特性的情况下就开始使用抽象层。抽象从具体事物开始,而不是相反,你必须了解不止一个事物(不仅仅是EF)才能构建有意义的抽象。如果他只看到了狗,没有见过猫,那么他就不会有动物的想法。 - Slauma
1
我同意。我一直使用存储库模式,因为这是我学习的方式。但最近我意识到,在90%的用例中,它只是不必要的抽象。在我的最后一个项目中,我只是为dbContext类创建了一个接口,公开表、savechanges函数和任何其他我可能需要的额外内容。 - edgarian
3
仓库抽象还有另一个目的。它抽象了数据的查询/创建方式。例如,如果您需要从数据库中没有的其他数据构建实体,则使用仓库的层不会更改,并且不会知道其接收到的数据是如何构建的或来自哪里。 - eran otzap
7
你觉得在实际工作中,更换ORM的频率应该是多少? - Blake Mumford
1
这不仅仅是为了ORM而存在的。我们有几个数据上下文没有使用EF。例如,我们使用我们的repos来处理Lucene、平面文件和主机访问。它允许我们的基础设施扩展到其他数据源。我认为当处理多个数据源时,存储库模式非常有用。无论持久性来源如何,我们都可以从相同的功能中受益。 - DDiVita
显示剩余5条评论

9

这张图片很容易理解:

在此输入图片描述


3
在EF中,数据库上下文遵循工作单元模式,每个集合类似于一个存储库。您可以创建一个非常简单的包装器来封装上下文并进行单元测试,同时保持不变。 - Richard Garside
您介意详细说明一下吗?DbContext、ISS、Unit of Work是什么?这样可以更好地解释图片。 - carloswm85

8

提高测试性并与底层持久化技术松耦合是一件事。但您还将为每个聚合根对象(例如,订单可以是聚合根,它还有订单行(不是聚合根))拥有一个存储库,以使域对象持久化更通用。

这也使得对象管理变得更加容易,因为当您保存订单时,它也将保存您的子项(可以是订单行)。


4
嗯,我仍然不太明白为什么每个聚合根对象部分都需要一个代码库。当我使用实体框架查询订单对象时,难道订单不会包含订单行列表吗?抱歉,我有点混淆了... - King Chan
在EF中,您还可以使用ObjectContext.SaveChanges()方法保存和检索完整的聚合根对象。但我写这篇文章是因为这是存储库模式的优点之一。 - Espen Burud
4
阅读此内容的人应知道,存储库模式是一种反模式。Ayende解释了原因: http://www.youtube.com/watch?v=0tlMTJDKiug - Sam
1
@SamDev 谢谢 - 我花了几个小时在网上冲浪,才找到一个能够清晰表达为什么我看到的存储库代码的大量增加让我感到不适的人。 - Michael12345
请远离仓储模式,你很可能会找到更好的方法。 - aggsol

5

把你的查询保存在一个中心位置也是有优势的;否则,你的查询会分散在各处,难于维护。

同时,你所提到的第一点:“隐藏EF”是非常好的!例如,保存逻辑可能很难实现。有多种策略可以在不同的场景下应用最佳。特别是当涉及到保存也对相关实体进行更改时。

使用存储库(与UnitOfWork结合使用)也可以将此逻辑集中起来。

这里有一些视频,其中有很好的解释。


4

仓库系统非常适合进行测试。

其中一个原因是您可以使用依赖注入。

基本上,您为存储库创建一个接口,并在创建对象时引用该接口。然后,您可以稍后创建一个模拟对象(例如使用moq),该模拟对象实现该接口。使用类似ninject的东西,您可以将正确的类型绑定到该接口。瞬间,您刚刚消除了一种依赖关系并用可测试的内容替换了它。

这个想法是能够轻松地交换对象的实现以进行测试


0

我知道在这里提供链接是不好的,但是我想分享一下这个视频,它解释了在使用实体框架时使用存储库模式的各种优点。以下是YouTube的链接。

https://www.youtube.com/watch?v=rtXpYpZdOzM

它还提供了有关如何正确实现仓储模式的详细信息。


1
你现在在 Entity Framework Core 中不再需要使用仓储模式进行测试,除非你想要将 EF 实现从业务层中隐藏。 - sensei

0
同样的原因,你不应该在应用程序中硬编码文件路径:松耦合和封装。想象一下一个应用程序,其中硬编码引用了“c:\windows\fonts”,这可能会导致什么问题。你不应该硬编码路径引用,那么为什么要硬编码对持久层的引用呢?将路径隐藏在配置设置(或特殊文件夹或操作系统支持的任何内容)后面,并将持久性隐藏在存储库后面。如果持久性问题被隐藏在存储库后面,那么单元测试、部署到其他环境、交换实现以及推理域对象将变得更加容易。

-1

当你设计你的仓储类来看起来像领域对象,为所有的仓储提供相同的数据上下文,并促进工作单元的实现时,仓储模式就有意义了。请参考下面的一些人为的例子。

  class StudenRepository
  {
     dbcontext ctx;
     StundentRepository(dbcontext ctx)
     {
       this.ctx=ctx;
     }
     public void EnrollCourse(int courseId)
     {
       this.ctx.Students.Add(new Course(){CourseId=courseId});
     }
  }

  class TeacherRepository
  {
     dbcontext ctx;
     TeacherRepository(dbcontext ctx)
     {
       this.ctx=ctx;
     }
     public void EngageCourse(int courseId)
     {
       this.ctx.Teachers.Add(new Course(){CourseId=courseId});
     }
  }

  public class MyunitOfWork
  {
     dbcontext ctx;
     private StudentRepository _studentRepository;
     private TeacherRepository _teacherRepository;

     public MyunitOfWork(dbcontext ctx)
     {
       this.ctx=ctx;
     }

    public StudentRepository StundetRepository
    {
       get
       {       
             if(_studentRepository==null)
                _stundentRepository=new StundetRepository(this.ctx);

            return _stundentRepository;    
       }
    }

    public TeacherRepository TeacherRepository 
    {
       get
       {       
             if(_teacherRepository==null)
                _teacherRepository=new TeacherRepository (this.ctx);

            return _teacherRepository;    
       }
    }

    public void Commit()
    {
         this.ctx.SaveChanges();
    }
  }

//some controller method
public void Register(int courseId)
{
  using(var uw=new MyunitOfWork(new context())
  {
    uw.StudentRepository.EnrollCourse(courseId);
    uw.TeacherRepository.EngageCourse(courseId);
    uw.Commit();
  }
}

你应该解释一下你在上面为所问问题编写的代码。 - Manish Jain

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