为什么我不应该在实体框架中使用存储库模式?


203

在工作面试中,我被要求解释为什么存储库模式不是与诸如实体框架之类的ORM一起使用的好模式。为什么会这样呢?


60
这是一个棘手的问题
Omu,2012年

2
我可能会对采访者回答说,Microsoft在演示实体框架时经常使用存储库模式:| 。
Laurent Bourgault-Roy 2012

1
那么,面试官为什么不是一个好主意呢?
鲍勃·霍恩

3
有趣的事实是,在Google中搜索“存储库模式”会得到与Entity Framework以及与EF如何使用该模式有关的结果。
Arseni Mourzenko

2
检查ayende的博客ayende.com/blog。根据我的了解,他曾经使用存储库模式,但最终放弃了它,转而使用查询对象模式
Jaime Sangcap 2014年

Answers:


99

我看不出任何原因导致存储库模式不适用于Entity Framework。存储库模式是放在数据访问层上的抽象层。您的数据访问层可以是任何东西,从纯ADO.NET存储过程到Entity Framework或XML文件。

在大型系统中,数据来自不同的源(数据库/ XML / Web服务),最好有一个抽象层。在这种情况下,存储库模式效果很好。我认为实体框架不足以掩盖幕后发生的事情。

我已将Repository模式与Entity Framework一起用作我的数据访问层方法,但尚未遇到问题。

DbContext使用Repository 提取的另一个优势是单元可测试性。您可以IRepository拥有两个实现的接口,第一个实现(DbContext用于与数据库进行通信的真实存储库),第二个FakeRepository实现可以返回内存中的对象/模拟的数据。这使您可以进行IRepository单元测试,从而可以使用的其他代码部分IRepository

public interface IRepository
{
  IEnumerable<CustomerDto> GetCustomers();
}
public EFRepository : IRepository
{
  private YourDbContext db;
  private EFRepository()
  {
    db = new YourDbContext();
  }
  public IEnumerable<CustomerDto> GetCustomers()
  {
    return db.Customers.Select(f=>new CustomerDto { Id=f.Id, Name =f.Name}).ToList();
  }
}
public MockRepository : IRepository
{
  public IEnumerable<CustomerDto> GetCustomers()
  {
    // to do : return a mock list of Customers
    // Or you may even use a mocking framework like Moq
  }
}

现在使用DI,您将获得实现

public class SomeService
{
  IRepository repo;
  public SomeService(IRepository repo)
  {
     this.repo = repo;
  }  
  public void SomeMethod()
  {
    //use this.repo as needed
  }    
}

3
我没有说过它不起作用,我也使用过EF的存储库模式,但是今天我被问到为什么将模式与DataBase一起使用是不可行的,使用数据库的应用程序

2
好的,因为这是最受欢迎的答案,所以我将其选择为正确答案

65
上一次最受欢迎的==上次是什么时候?
HDave

14
DbContext已经是一个存储库,该存储库是低级抽象。如果要抽象不同的数据源,请创建对象来表示这些数据源。
丹尼尔·利特尔

7
ColacX。我们只是在控制器层尝试了DBcontext,然后恢复到回购模式。使用Repo模式,单元测试源自不断失败的大规模DbContext模拟。EF难以使用,并且对于EF细微差别的研究非常脆弱且耗时数小时。现在,我们有一些简单的回购模拟。代码更干净。分工更加清晰。我不再与众不同,EF已经是一种回购模式,并且已经可以进行单元测试。
Rhyous

434

不将存储库模式与Entity Framework结合使用的唯一最佳理由是什么?实体框架已经实现了存储库模式。DbContext是您的UoW(工作单元),每个DbSet都是存储库。在此之上实现另一层不仅是多余的,而且会使维护更加困难。

人们遵循模式而没有意识到模式的目的。对于存储库模式,其目的是抽象掉低级数据库查询逻辑。在以前在代码中实际编写SQL语句的年代,存储库模式是一种将SQL从分散在整个代码库中的单个方法中移出并将其本地化的一种方法。拥有诸如Entity Framework,NHibernate等之类的ORM可以替代此代码抽象,因此不需要模式。

但是,在您的ORM上创建抽象并不是一个坏主意,它没有UoW / repostitory那样复杂。我将采用一种服务模式,在此模式下,您可以构造一个应用程序可以使用的API,而无需知道或关心数据是来自实体框架,NHibernate还是Web API。这要简单得多,因为您只需在服务类中添加方法即可返回应用程序所需的数据。例如,如果您正在编写“待办事项”应用程序,则可能需要致电服务,以退回本周到期但尚未完成的项目。您的应用程序只知道如果需要此信息,它将调用该方法。在该方法内部以及一般在服务中,您将与Entity Framework或正在使用的其他任何对象进行交互。然后,如果您以后决定切换ORM或从Web API中提取信息,

听起来这可能是使用存储库模式的一个潜在论点,但是这里的主要区别在于服务是一个较薄的层,旨在返回完全烘焙的数据,而不是您继续查询的内容,例如资料库。


68
这似乎是唯一正确的答案。
Mike Chamberlain 2014年

10
可以DbContext在EF6 +中进行模拟(请参阅:msdn.microsoft.com/en-us/data/dn314429.aspx)。即使在较小的版本中,由于实现了iterface,您也可以将DbContextDbSets 用作伪类。DbSetIDbSet
克里斯·普拉特

14
@TheZenker,您可能未完全遵循存储库模式。最严格的区别是返回值。存储库返回可查询的内容,而服务应返回可枚举的内容。即使那不是真的那么黑白,因为那里有些重叠。更多有关如何使用它的信息。存储库应该只返回所有对象的集合,然后您可以进一步查询这些对象,而服务应该返回最终的数据集,并且不应该支持进一步的查询。
克里斯·普拉特

10
冒着自负的风险:他们错了。现在,就官方教程而言,微软已经放弃使用自EF6以来见过的存储库。关于这本书,我无法说出为什么作者选择使用存储库。作为正在开发大型应用程序的人,我可以说的是,将存储库模式与Entity Framework一起使用是维护的噩梦。一旦进入比少数几个存储库更复杂的事物,最终将花费大量时间来管理存储库/工作单元。
克里斯·普拉特

6
通常每个数据库或访问方法只有一项服务。我使用通用方法从同一组方法中查询多个实体类型。我使用Ninject将上下文注入我的服务,然后将我的服务注入我的控制器,因此一切都变得整洁。
克里斯·普拉特

45

这是来自Ayende Rahien的一张照片:在厄运的深渊中进行架构设计:存储库抽象层的弊端

我不确定我是否同意他的结论。这是一个陷阱22-一方面,如果我使用查询特定的数据检索方法将我的EF Context包装在特定于类型的存储库中,则实际上我可以对我的代码进行某种形式的单元测试,这对于Entity几乎是不可能的仅框架。另一方面,我失去了进行丰富的关系查询和语义维护的能力(但是,即使我可以完全访问这些功能,我也总是觉得自己像是在EF或我可能选择的任何其他ORM周围的蛋壳上行走,因为我从不知道其IQueryable实现可能支持或可能不支持哪些方法,因此它将是将我添加到导航属性集合中的内容解释为创建还是仅将其解释为是懒散的还是渴望加载的,或者根本不加载默认,等等。所以也许这是更好的。零阻抗对象关系“映射”是一种神话生物-也许这就是为什么最新版本的Entity Framework代号为“ Magic Unicorn”的原因。

但是,通过特定于查询的数据检索方法来检索您的实体意味着您的单元测试现在基本上是白盒测试,因此您别无选择,因为您必须事先确切知道被测单元将使用哪种存储库方法。呼叫以模拟它。而且,除非您还编写集成测试,否则您实际上还没有真正测试查询本身。

这些都是复杂的问题,需要复杂的解决方案。您不能仅通过假装所有实体都是单独的类型并且彼此之间没有任何关系并将它们原子化到各自的存储库中来进行修复。那么你可以,但它吮吸。

更新:使用实体框架的Effort提供程序取得了一些成功。Effort是一个内存中提供程序(开放源代码),可让您完全按照对实际数据库使用EF的方式在测试中使用EF。我正在考虑使用该提供程序来切换该项目中的所有测试,因为它似乎使事情变得如此简单。这是迄今为止我发现的唯一解决方案,它可以解决我先前提出的所有问题。唯一的问题是,在开始测试时会稍有延迟,因为它正在创建内存数据库(它使用另一个名为NMemory的软件包来执行此操作),但是我认为这不是真正的问题。有一篇代码项目文章讨论了如何使用Effort(相对于SQL CE)进行测试。


3
任何不提及单元测试的体系结构文章都会自动发送给我的垃圾箱。存储库模式的要点之一是获得一定的测试能力。
睡眠者史密斯,

3
您仍然可以进行单元测试,而无需包装EF上下文(已经是存储库)。您应该对域/服务进行单元测试,而不是对数据库查询进行单元测试(它们是集成测试)。
丹尼尔·利特尔

2
EF的可测试性在版本6中得到了极大的改进。您现在可以完全嘲笑了DbContext。无论如何,您总是可以嘲笑DbSet,无论如何,这就是Entity Framework的实质。DbContext仅是一个类,用于DbSet在一个位置(工作单元)中存储属性(存储库),尤其是在单元测试环境中,该环境始终不需要或不需要所有数据库初始化和连接的东西。
克里斯·普拉特

松散相关实体导航很不好,而且是面向对象操作,但您将对所查询的内容拥有更多控制权。
Alireza 2015年

到测试点为止,EF Core在开箱即用的内存中和带有Sqlite提供程序的内存中已经实现了很长的路要进行单元测试。需要进行集成测试以在容器化数据库上运行测试时,请使用docker。
Sudhanshu Mishra

16

您可能会这样做的原因是因为它有点多余。实体框架为您提供了丰富的编码和功能优势,这就是为什么要使用它,如果再采用它并将其包装在存储库模式中,则会丢掉这些优势,那么您可能还会使用任何其他数据访问层。


您能否谈谈“实体框架为您提供大量编码和功能上的优势”的一些优势?
ManirajSS 2015年

2
这就是他的意思。var id = Entity.Where(i => i.Id == 1337).Single()封装并将其包装在存储库中,您基本上无法从外部进行这样的查询逻辑,这要么迫使您向A添加更多代码存储库和用于获取ID的接口。B从存储库返回实体上下文,以便您可以编写查询逻辑(这只是废话)
ColacX 2016年

14

从理论上讲,我认为封装数据库连接逻辑以使其更易于重用是有意义的,但是正如下面的链接所述,我们的现代框架现在基本上已经在处理此问题。

重新考虑存储库模式


我喜欢这篇文章,但是对于企业应用程序而言,恕我直言,DAL和Bl之间的抽象层必须具有功能,因为您不知道明天将确切使用什么。但是,感谢您共享链接

1
虽然我个人认为NHibernate是正确的(ISessionFactory并且ISession很容易嘲笑),但是DbContext不幸的是,这并不容易...
PatrykĆwiek2012年

6

使用存储库模式的一个很好的理由是允许将业务逻辑和/或UI与System.Data.Entity分离。这有很多优点,包括允许他使用Fakes或Mocks进行单元测试的实际好处。


我同意这个答案。我的存储库基本上只是扩展方法,除了构建表达式树外什么都不做。通过非常简单的抽象,该抽象直接在dbcontext顶部直接提供了通用功能。抽象的唯一真正目的是使IoC更加容易。我认为人们尝试在存储库中执行不应执行的操作。他们根据每个实体进行回购,或将业务逻辑放在应该在服务层中的位置。您实际上只需要一个简单的通用存储库。它不是必须的,只需提供一致的接口即可。
布兰登

我只想添加一件事。是的,在大多数情况下,CQRS是一种非常优越的方法。对于我工作过的某些客户,当数据库专家不能与开发人员很好地合作时(这种情况发生的频率比人们想象的要多,尤其是在银行),EF over SQL是最好的选择。在这种特定情况下,当您完全无法控制数据库时,存储库模式就很有意义。因为它非常类似于数据结构,并且很容易将发生的事情转换为数据库,反之亦然。我认为这确实是一个政治和后勤决定。为了安抚DB之神。
布兰登

1
实际上,我开始对此提出质疑。EF是工作单元和存储库模式的组合。就像Chris Pratt上面在EF6中提到的那样,您可以轻松地模拟Context和DbSet对象。我仍然认为,应该将数据访问包装在类中,以保护业务逻辑类免受实际的数据访问机制的侵害,但是如果将整个EF打包并用另一个存储库包装EF,则工作单元抽象似乎是过大的选择。
James Culshaw

我不认为这是一个好答案,因为您的支持性陈述仅列出一个具有很多优点。您列出的一个优点不是很好的理由,因为您可以使用内存数据库来进行实体操作单元测试。
乔尔·麦克贝斯

@jcmcbeth,如果您直接在上方查看我的评论,您会发现我对存储库模式和EF的最初看法已经改变。
James Culshaw

0

我们遇到了重复但不同的Entity Framework DbContext实例的问题,当每种类型的new()向上存储库的IoC容器(例如,每个用户从DBContext调用自己的IDbSet的UserRepository和GroupRepository实例)有时可能会导致每个请求多个上下文(在MVC / Web上下文中)。

在大多数情况下,它仍然有效,但是当您在其上添加服务层并且这些服务假定使用一个上下文创建的对象将作为子集合正确附加到另一个上下文中的新对象时,它有时会失败,有时却不会。 t取决于提交的速度。


我在几个不同的项目中都遇到过这个问题。
ColacX

0

在小型项目上尝试了存储库模式后,我强烈建议不要使用它;不是因为它使您的系统变得复杂,不是因为模拟数据是噩梦,而是因为您的测试变得无用!

通过模拟数据,您可以添加不带标题的详细信息,添加违反数据库约束的记录以及删除数据库拒绝删除的实体。在现实世界中,单个更新可能会影响多个表,日志,历史记录,摘要等,以及诸如上次修改日期字段,自动生成的键,计算字段之类的列。

简而言之,在真实数据库上运行测试将为您带来真实的结果,并且不仅可以测试服务和接口,还可以测试数据库行为。您可以检查存储过程是否对数据进行了正确的处理,是否返回了预期的结果,或者您发送删除的记录是否确实被删除了!这样的测试还会暴露出诸如忘记从存储过程中引发错误等问题,以及成千上万的此类情况。

我认为,实体框架实现的存储库模式要比我到目前为止阅读的任何文章都更好,并且远远超出了他们试图完成的工作。

在我们使用XBase,AdoX和Ado.Net并使用实体的日子里,存储库是最佳实践!(存储库超过存储库)

最后,我认为太多的人在学习和实现存储库模式上投入了大量时间,他们拒绝放手。主要是为了向自己证明他们没有浪费时间。


1
除了您不想在单元测试中测试数据库行为外,因为它完全不是那种测试级别。
Mariusz Jamro'3

是的,您在这里谈论的是集成测试,这确实很有价值,但是单元测试完全不同。您的单元测试永远不要访问真实的数据库,但是建议您添加可以的集成测试。
克里斯·普拉特

-3

这是由于迁移所致:由于连接字符串位于web.config中,因此无法使迁移正常进行。但是,DbContext驻留在存储库层中。IDbContextFactory需要对数据库具有配置字符串。但是,迁移无法从web.config获取连接字符串。

有解决方法,但是我还没有找到一个干净的解决方案!

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.