存储库模式最佳实践

时间:2009-08-28 15:20:37

标签: asp.net repository-pattern repository repository-design

所以我在应用程序中实现了存储库模式,并且在我对模式的理解中遇到了两个“问题”:

  1. 查询 - 我已阅读使用存储库时不应使用IQueryable的响应。但是,显而易见的是,您希望每次调用方法时都不返回完整的对象列表。应该实施吗?如果我有一个名为List的IEnumerable方法,那么IQueryable的一般“最佳实践”是什么?应该/不应该有哪些参数?

  2. 标量值 - 什么是最好的方法(使用存储库模式)返回单个标量值而不必返回整个记录?从性能的角度来看,在整行上只返回一个标量值会不会更有效率?

3 个答案:

答案 0 :(得分:32)

严格地说,Repository提供了获取/放置域对象的集合语义。它提供了围绕实现实现(ORM,手动,模拟)的抽象,以便域对象的使用者与这些细节分离。在实践中,存储库通常抽象对实体的访问,即具有标识的域对象,并且通常是持久的生命周期(在DDD风格中,存储库提供对聚合根的访问)。

存储库的最小接口如下:

void Add(T entity);
void Remove(T entity);
T GetById(object id);
IEnumerable<T> Find(Specification spec);

虽然你会看到命名差异和添加Save / SaveOrUpdate语义,但上面是'纯'的想法。您将获得ICollection添加/删除成员以及一些查找程序。如果您不使用IQueryable,您还会在存储库中看到finder方法,如:

FindCustomersHavingOrders();
FindCustomersHavingPremiumStatus();

在此上下文中使用IQueryable存在两个相关问题。第一种是以域对象的关系形式向客户泄露实现细节的可能性,即违反Demeter法则。第二个是存储库获取可能不属于域对象存储库的查找职责,例如,查找与所请求的域对象相关的投影而不是相关数据。

此外,使用IQueryable'打破'模式:具有IQueryable的存储库可能提供也可能不提供对“域对象”的访问。 IQueryable为客户提供了许多关于在最终执行查询时将实现的内容的选项。这是关于使用IQueryable的争论的主要内容。

关于标量值,您不应使用存储库来返回标量值。如果你需要一个标量,你通常会从实体本身得到它。如果这听起来效率低,那就是,但您可能不会注意到,这取决于您的负载特性/要求。如果您需要域对象的备用视图,由于性能原因或者您需要合并来自许多域对象的数据,您有两种选择。

1)使用实体的存储库查找指定的实体,并将项目/地图映射到展平视图。

2)创建一个finder接口,专门用于返回一个新的域类型,该类型封装了您需要的展平视图。这不是一个存储库,因为没有Collection语义,但它可能会使用现有的存储库。

如果使用“纯”存储库访问持久化实体,则需要考虑的一件事是,您要牺牲ORM的一些好处。在“纯”实现中,客户端无法提供域对象如何使用的上下文,因此您无法告诉存储库:'嘿,我只是要更改customer.Name属性,所以不要得到那些急切加载的参考资料。另一方面,问题是客户是否应该知道这些东西。这是一把双刃剑。

就使用IQueryable而言,大多数人似乎都习惯于“打破”模式以获得动态查询组合的好处,尤其是对于诸如分页/排序之类的客户端职责。在这种情况下,您可能有:

Add(T entity);
Remove(T entity);
T GetById(object id);
IQueryable<T> Find();

然后您可以取消所有这些自定义Finder方法,这些方法会随着查询要求的增长而使存储库变得混乱。

答案 1 :(得分:8)

为了回应@lordinateur,我真的不喜欢defacto指定存储库接口的方法。

因为解决方案中的接口要求每个存储库实现至少需要添加,删除,GetById等。现在考虑一种情况,通过存储库的特定实例保存它是没有意义的,你仍然必须使用NotImplementedException或类似的东西来实现其余的方法。

我更喜欢拆分我的存储库接口声明,如下所示:

interface ICanAdd<T>
{
    T Add(T entity);
}

interface ICanRemove<T>
{
    bool Remove(T entity);
}

interface ICanGetById<T>
{
    T Get(int id);
}

SomeClass实体的特定存储库实现可能如下所示:

interface ISomeRepository
    : ICanAdd<SomeClass>, 
      ICanRemove<SomeClass>
{
    SomeClass Add(SomeClass entity);
    bool Remove(SomeClass entity);
}

让我们退后一步,看看为什么我认为这比在一个通用界面中实现所有CRUD方法更好。

  

有些对象有所不同   要求比其他人。一个客户   对象可能无法删除,a   PurchaseOrder无法更新,而且   ShoppingCart对象只能是   创建。当一个人使用通用   这个IRepository接口   显然会导致问题   实现。

     

那些实施反模式的人   经常会全力实施   然后接口将抛出异常   对于他们没有的方法   支持。除了不同意   这打破了许多OO原则   他们希望能够使用他们的   有效的抽象抽象   除非他们也开始采用方法   关于它是否给定对象   得到支持并进一步实施   它们。

     

此问题的常见解决方法是   转向更细粒度的界面   例如ICanDelete,ICanUpdate,   ICanCreate等等   解决许多问题   在OO方面如雨后春笋般涌现   原则也大大减少了   正在进行的代码重用量   大部分时间都看不到   能够使用存储库   具体实例了。

     

我们都不喜欢写相同的代码   一遍又一遍。但是一个存储库   合同是建筑缝   是扩大的错误的地方   签订合同使其更具通用性。

这些摘录是从this post羞耻地获取的,您可以在评论中阅读更多讨论。

答案 2 :(得分:4)

关于1: 据我所知,IQuerable本身并不是从存储库返回的问题。存储库的重点是它应该看起来像一个包含所有数据的对象。因此,您可以向存储库询问数据。如果您有多个对象需要相同的数据,则存储库的工作是缓存数据,因此存储库的两个客户端将获得相同的实例 - 因此,如果一个客户端更改属性,另一个将看到,因为他们指的是同一个实例。

如果存储库实际上是Linq提供者本身,那么它就适合。但是大多数人只是让Linq-to-sql提供者的IQuerable直接通过,这实际上绕过了存储库的责任。所以存储库根本不是存储库,至少根据我对模式的理解和使用情况。

关于2: 当然,从数据库返回单个值比整个记录更有效。但是使用存储库模式,根本不会返回记录,您将返回业务对象。因此,应用程序逻辑不应该关注字段,而应关注域对象。

但是,与完整域对象相比,返回单个值的效率如何?如果您的数据库模式定义合理,您可能无法衡量差异。

拥有干净,易于理解的代码更为重要 - 而不是预先进行微观性能优化。