【问题标题】:What specific issue does the repository pattern solve?存储库模式解决了哪些具体问题?
【发布时间】:2023-03-06 21:38:01
【问题描述】:

(注意:我的问题与三个月前问this question 的人非常相似,但从未得到回答。)

我最近开始使用 MVC3 + 实体框架,我一直在阅读,最佳实践是使用存储库模式来集中访问 DAL。这还伴随着您希望将 DAL 与域(尤其是视图层)分开的解释。但在我看到的示例中,存储库(或似乎是)只是返回 DAL 实体,即在我的情况下,存储库将返回 EF 实体。

所以我的问题是,如果存储库只返回 DAL 实体有什么好处?这不会增加一层复杂性,不会消除在层之间传递 DAL 实体的问题吗?如果存储库模式创建了“进入 DAL 的单一入口点”,那与上下文对象有何不同?如果存储库提供了一种机制来检索和持久化 DAL 对象,那么它与上下文对象有何不同?

此外,我在至少一个地方读到工作单元模式集中存储库访问以管理数据上下文对象,但我也不明白为什么这很重要。

我 98.8% 肯定我在这里遗漏了一些东西,但从我的阅读中我没有看到它。当然,我可能只是没有阅读正确的资料...:\

【问题讨论】:

    标签: entity-framework design-patterns domain-driven-design repository-pattern


    【解决方案1】:

    我认为术语“存储库”通常被认为是在 Martin Fowler 的书Patterns of Enterprise Application Architecture 中描述的“repository pattern”。

    存储库在域和数据映射层之间进行中介, 充当内存中的域对象集合。客户对象 以声明方式构造查询规范并将它们提交给 满意的存储库。可以添加和删除对象 存储库,因为它们可以来自简单的对象集合,以及 Repository 封装的映射代码将执行 在幕后进行适当的操作。

    从表面上看,Entity Framework 完成了所有这些,并且可以用作存储库的简单形式。但是,存储库可能不仅仅是数据层抽象。

    根据 Eric Evans 的《Domain Driven Design》一书,存储库具有以下优点:

    • 他们为客户提供了一个简单的模型,用于获取持久性对象并管理其生命周期
    • 它们将应用程序和域设计与持久性技术、多种数据库策略甚至多个数据源解耦
    • 他们交流有关对象访问的设计决策
    • 它们允许轻松替换虚拟实现以进行单元测试(通常使用内存中的集合)。

    第一点大致相当于上面那一段,不难看出Entity Framework本身很容易做到。

    有些人会认为 EF 也完成了第二点。但通常 EF 仅用于将每个数据库表转换为 EF 实体,并将其传递给 UI。它可能抽象了数据访问的机制,但几乎没有抽象出幕后的关系数据结构。

    在主要面向数据的更简单的应用程序中,这似乎不是重点。但是随着应用程序的域规则/业务逻辑变得更加复杂,您可能希望更加面向对象。数据的关系结构包含对业务领域不重要但属于数据存储的副作用的特性并不罕见。在这种情况下,仅仅抽象持久性机制是不够的,还要抽象数据结构本身的性质。单独的 EF 通常无法帮助您做到这一点,但存储库层可以。

    至于第三个优势,EF 不会(从 DDD 的角度)提供帮助。通常 DDD 不仅使用存储库来抽象数据持久性的机制,而且还提供有关如何访问某些数据的约束:

    我们也不需要对更多的持久对象进行查询访问 方便遍历查找。例如,一个人的地址 可以从 Person 对象请求。最重要的是,任何 禁止访问 AGGREGATE 内部的对象,除非通过 从根遍历。

    换句话说,你不会因为你的数据库中有一个地址表而有一个'AddressRepository'。如果您的设计选择以这种方式管理 Address 对象的访问方式,那么您将在 PersonRepository 中定义和实施设计选择。

    此外,DDD 存储库通常是封装与域数据集相关的某些业务概念的地方。 OrderRepository 可能有一个名为OutstandingOrdersForAccount 的方法,它返回一个特定的Orders 子集。或者,客户存储库可能包含 PreferredCustomerByPostalCode 方法。

    如果没有添加存储库抽象层,Entity Framework 的 DataContext 类将无法很好地支持此类功能。它们适用于 DDD 所谓的规范,它可以是简单的布尔表达式,发送到一个简单的方法,该方法将根据表达式评估数据并返回匹配项。

    至于第四个优势,虽然我确信有某些策略可以让一个替代数据上下文,但将其包装在存储库中使其变得非常简单。

    关于“工作单元”,这是 DDD 书中所说的:

    将事务控制权留给客户端。 虽然 REPOSITORY 会在数据库中插入和删除,但通常不会 承诺任何事情。保存后提交是很有诱惑力的,例如, 但客户可能具有正确启动和 提交工作单元。事务管理会更简单,如果 REPOSITORY 不插手。

    【讨论】:

    • 这是一个很棒的回应,谢谢。我在周末拿起了Pro ASP.NET MVC3 Framework 一书,因为它是从头开始的 DD 并且讲的方式大致相同。我确实认为这是正确的方法,我只是想拼凑具体的细节,例如直到本周末,才终于清楚存储库返回的是域对象,而不是 DAL 对象。之前对 EF 的研究真的把这里的水弄得一团糟,这导致我从 EF 部分类中制作“域”对象,这闻起来真的很糟糕。再次感谢您的评论,我会定期重读。
    • 很好的答案,我一直说存储库和 DAO 之间的区别在于存储库提供对聚合根的访问。这是一个微妙但非常重要的区别。聚合根不仅仅是简单的 DTO。他们有能力在该领域做真正的工作。
    • 我认为混淆归结为实体框架可能不是一个完整的存储库因为它最常用。但是,您在这里提到的任何事情都不能DbContext来完成。
    • 我明白这一点。 DbContext 是 UoW,每个 DbSet 是一个存储库。但是,这是关键,DbContext 只是一个类。你可以在里面放任何你想要的属性,DbSet 或者不。因此,您可以继承 DbSet 并创建您想要的任何方法,或者您可以基于 IDbSet 滚动您自己的方法并在您的上下文中使用它。您可以直接与数据库交互,提供调用存储过程的方法并返回功能与任何旧的DbSet 类似的对象集合等。有很多潜力,但很少有人真正利用。
    • @ChrisPratt 好吧,我们有点同意。问题是,套用一句话,“我有一个自称为存储库的上下文,但它必须有更多,我错过了什么?”。答案是,您在 ORM 之上添加的所有其他内容,以使其成为实际存储库。但是当你说“每个DbSet 都是一个存储库”时,我不得不强烈反对。它只是对数据库表的抽象,但这并不能使其成为存储库。
    【解决方案2】:

    Entity Framework 的DbContext 基本上类似于一个存储库(以及一个工作单元)。您不必在简单的场景中将其抽象出来。

    存储库的主要优点是您的域可以是无知的并且独立于持久性机制。在基于层的架构中,依赖关系从 UI 层向下通过域(或通常称为业务逻辑层)指向数据访问层。这意味着 UI 依赖于 BLL,而 BLL 本身又依赖于 DAL。

    在更现代的架构中(通过域驱动设计和其他面向对象的方法传播),域应该没有向外指向的依赖项。这意味着 UI、持久性机制和其他一切都应该依赖于域,而不是相反。

    然后,存储库将通过其在域内的接口表示,但在域外的持久性模块中具有其具体实现。这样域只依赖于抽象接口,而不是具体的实现。

    这基本上是面向对象与架构级别的过程编程。

    另请参阅Ports and Adapters a.k.a. Hexagonal Architecture

    存储库的另一个优点是您可以为各种数据源创建类似的访问机制。不仅适用于数据库,还适用于基于云的商店、外部 API、第三方应用程序等。

    【讨论】:

    • 在什么时候将其抽象出来会有好处?例如,我最近在我的应用程序中添加了第二个实体模型。它指向同一个数据库,但仅引用我为帮助填充某些报告屏幕而创建的视图。当您有多个 EF 上下文在使用时,迁移到存储库会更好吗?如果是这样,存储库对象是否应该简单地保存指向上下文的指针,或者为每种类型的对象实现全范围的“FindById”、“Save”等?对我来说似乎有点矫枉过正。谢谢。
    • @Dave 提供视图的单独上下文不需要存储库(它也不需要域,因为显示数据可能涉及的业务逻辑并不多)。只需让 UI 直接使用该 DbContext 来检索视图,可能会按用户、组或其他限制进行过滤。我想建议您在此处查看 CQRS 架构风格(只需在 Google 上搜索该术语)。我在上面扩展了我的答案,试图解释存储库的目的。
    • 存储库不仅仅是域模型和持久存储之间的抽象层。这是一种确保实体通过其聚合根获得的机制。没有它,它只是 DAO 的另一个名称吗?
    • 没错。存储库(在 DDD 中)将与持久性相关的对象(ORM 的表类、事件流等)转换为域模型的聚合(并返回)。否则,它只是 DAO 的一个花哨的名称。
    • @Dennis ORM 的工作是将域模型转换为与持久性相关的对象。制作您的域模型,然后使用 Fluent 配置将它们映射到数据库对象。没有规定您的 EF 实体必须与您的数据库表匹配。
    【解决方案3】:

    您是对的,在这些简单的情况下,存储库只是 DAO 的另一个名称,它只带来一个价值:您可以将 EF 切换到另一种数据访问技术。今天你使用 MSSQL,明天你会想要一个云存储。或者使用 micro orm 而不是 EF 或从 MSSQL 切换到 MySql。

    在所有这些情况下,您最好使用存储库,因为应用程序的其余部分不会关心您现在使用的存储空间。

    在有限的情况下,您从多个来源(db + 文件系统)获取信息,repo 将充当外观,但它仍然是 DAO 的另一个名称。

    “真实”存储库仅在您处理域/业务对象时有效,对于不会更改存储的以数据为中心的应用程序,仅 ORM 就足够了。

    【讨论】:

      【解决方案4】:

      这在您有多个数据源并希望使用一致的编码策略访问它们的情况下很有用。

      例如,您可能有多个 EF 数据模型,一些数据使用带有存储过程的传统 ADO.NET 访问,一些数据使用第 3 方 API 访问,还有一些从 Windows NT4 服务器上的 Access 数据库访问坐在扫帚柜里的灰尘毯下。

      您可能不希望您的业务或前端层关心数据的来源,因此您构建了一个通用存储库模式来访问“数据”,而不是访问“实体框架数据”。

      在这种情况下,您的实际存储库实现会彼此不同,但调用它们的代码不会知道差异。

      【讨论】:

      • 好的,我完全理解这一点。但是存储库是否仍在创建和使用 EF(或 ADO 或其他)对象?还是应该创建和使用“中性”对象?即我是否希望仅为存储库创建另一层映射?如果没有,我仍然会通过我的图层传递 EF(...等...)对象,所以我不确定我得到了什么。
      • 我会说实施完全取决于您。就个人而言,我喜欢在将实体返回给业务层或前端之前将它们转换为普通对象(DTO/POCO/等),因为我的数据库模型不一定是我的业务模型。但是,如果您构建表和实体以准确反映您需要在各层之间传递的数据,那将为您省去一些麻烦。但在我的示例中,由于一些数据来自 EF 而一些不是,因此如果一些存储库返回“实体”而另一些没有。
      • 好点。我正在考虑为我的应用程序构建域模型。我还在直接在视图中使用实体进行原型设计,但我也在尝试摆脱这种情况。
      • 我不明白你说的传递 EF 对象是什么意思。这些只是带有字段的普通类对象。它们对 EF 来说并不特别。 EF 只是将数据映射到它们,就像您在使用 ADO.NET 时手动执行的操作一样。然后您的服务层使用这些对象。无论您拥有什么后端,都将使用同一类数据。这就是为什么嘲笑它们和测试有效的原因。您在测试时没有使用 EF,但它可以工作,因为它们只是普通的 C# 对象,其中有数据映射到其中。
      • @user441521 在较旧的 EF 版本中,实体类是特殊的,派生自 EntityObject,自我跟踪更改,我相信与上下文密切相关 - 所以你不能引用这些类之一来自您的其他应用程序层,而无需同时引用 EF 和您的上下文。代码优先、POCO T4 模板以及他们对 EF 所做的其他大规模检修可能会改变这一点,但即便如此,您的实体仍然代表“数据库数据”,这可能不是您想要向其他层公开的内容(例如,您可能想省略一些列)。
      【解决方案5】:

      鉴于您的情况,我会简单地选择一组接口来表示需要从数据层返回的数据结构(您的领域模型)。然后,您的实现可以是 EF、Raw ADO.Net 或任何其他类型的数据存储/提供程序的混合。这里的关键策略是将实现从直接消费者(您的域层)中抽象出来。当您想要对域对象进行单元测试时,这很有用,并且在不太常见的情况下 - 完全更改您的数据提供者/数据库平台。

      如果您还没有,您应该考虑使用IOC container,因为它们通过Dependency Injection 使您的解决方案的松散耦合变得非常容易。 There are many available,我个人更喜欢Ninject

      领域层应该封装你所有的业务逻辑——问题领域的规则和要求,并且可以直接被你的 MVC3 Web 应用程序使用。在某些情况下,引入位于域层之上的services layer 是有意义的,但这并不总是必要的,而且对于简单的 Web 应用程序来说可能是多余的。

      【讨论】:

      • 是的,我还没有进阶到 IoC,但这是我在不久的将来与 ValueInjecter 一起探索的事情清单。您关于包含业务逻辑的域模型的 cmets 很有意义。谢谢。
      • 最重要的是,您要勤奋并在具体解决方案的上下文中质疑这些模式的实用性,而不是因为它们是热门话题而盲目使用它们。祝你的项目好运。
      【解决方案6】:

      要考虑的另一件事是,即使您知道您将使用单个数据存储,创建存储库抽象仍然可能有意义。原因是您的应用程序可能需要一个功能,而您的 ORM du jour 要么表现不佳(性能),要么根本没有,或者您只是不知道如何使 ORM 满足您的需求。

      如果您将 ORM 包装在经过深思熟虑的存储库接口之后,您可以轻松地在您认为合适的不同技术之间切换。在我的存储库中看到一些方法使用 EF 进行工作而其他方法使用 PetaPoco 或(喘气)ADO.net 代码之类的方法并不少见。存储库抽象使您能够为手头的工作使用正确的工具,而不会将这些复杂性泄漏到客户端代码中。

      【讨论】:

      • 感谢您的评论。我想你在我第一次问这个问题时发现了一些我没有看到的东西。一旦我使用了一段时间,灯泡就熄灭了——存储库返回域对象,而不是 EF 实体。所以我最终创建了域存储库和一组域类,“重复工作”,但前提是你认为工作是尽量减少类的数量;在 DDD 中并非如此。它最终变得更加灵活和可用。是的,我确实也有一些存储库“帮助”方法。专为进行过滤等操作而设计。
      【解决方案7】:

      我认为对许多文章所说的“存储库”存在很大的误解。这就是为什么人们怀疑这些抽象带来的真正价值。

      在我看来,纯粹形式的存储库是 IEnumerable,而您和许多文章都在谈论“数据访问服务”。

      我已经在博客here

      【讨论】:

      • 我明白你的意思,但我认为这里的问题是用于定义该术语的不同上下文。领域驱动设计对存储库的定义显然与 Fowler 不同,或者至少该定义已扩展为包含您所描述的数据访问服务。上下文很重要。但是您对 IEnumerable 的观点很有趣。在我的案例中,实现域模型存储库极大地释放了灵活性、生产力和功能方面的收益。也许我本可以实现 IEnumerable ,但现在这是桥下的水。 :)
      • 实际上很难实现 Fowler 描述的存储库并使其在真实数据库上工作。今天只有一些 ORM 能够接近它,但没有人创建过许多使用内存对象集合和后端数据库的场景的完整实现。我计划在博客上讨论它以及为什么 DDD 不适合 99% 的 Web 应用程序。
      • 我很好奇,你写过那篇博文吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-09
      • 1970-01-01
      相关资源
      最近更新 更多