【问题标题】:How to use the repository pattern correctly?如何正确使用存储库模式?
【发布时间】:2010-12-02 12:08:48
【问题描述】:

我想知道我应该如何对我的存储库进行分组?就像我在 asp.net mvc 和我的书中看到的示例一样,它们基本上每个数据库表使用一个存储库。但这似乎有很多存储库导致您必须在以后调用许多存储库来进行模拟和其他东西。

所以我猜我应该将它们分组。但是我不确定如何对它们进行分组。

现在我创建了一个注册存储库来处理我所有的注册内容。但是,在我有 3 个存储库来执行此操作之前,我需要更新 4 个表。

例如,其中一个表是许可证表。当他们注册时,我查看他们的密钥并检查它是否存在于数据库中。现在,如果我需要在注册以外的其他地方检查此许可证密钥或该表中的其他内容,会发生什么情况?

一个地方可以登录(检查密钥是否未过期)。

那么在这种情况下我该怎么办?再次重写代码(break DRY)?尝试将这两个存储库合并在一起,并希望在其他某个时间点不需要任何方法(例如,也许我可能有一个方法来检查是否使用了 userName - 也许我会在其他地方需要它)。

此外,如果我将它们合并在一起,我要么需要 2 个服务层进入同一个存储库,因为我认为拥有一个站点的 2 个不同部分的所有逻辑会很长,而且我必须有像 ValidateLogin() 这样的名称, ValdiateRegistrationForm()、ValdiateLoginRetrievePassword() 等

或者无论如何调用存储库,只是有一个听起来很奇怪的名字?

似乎很难创建一个具有足够通用名称的存储库,以便您可以在应用程序的许多地方使用它并且仍然有意义,我认为在存储库中调用另一个存储库不是一个好习惯?

【问题讨论】:

  • +1。好问题。
  • 感谢它困扰了我一段时间。因为我发现我在书中看到的那些太简单了,所以它们没有告诉你在这些情况下该怎么做。
  • 我基本上和你做的一样,是的,如果我有一个在多个存储库中使用的 linq2sql 类,我需要更改我破坏 DRY 的表结构。不太理想。我现在计划得更好一点,所以我不需要多次使用 linq2sql 类,我认为这是很好的分离关注点,但我预见到有一天这对我来说将是一个真正的问题。
  • 我在这里问了一个类似的问题(但不完全相同):stackoverflow.com/questions/910156/…
  • 是的,但似乎很难针对所有情况进行计划。就像我说的那样,我可以将登录和注册合并到一个身份验证中,并有 2 个单独的层。这可能会解决我的问题。但是,如果在说我网站的个人资料页面时我想以某种原因向他们展示他们的密钥(也许我会让 htem 改变它或其他什么)会发生什么。现在我该怎么做才能打破 DRY 并写同样的东西?或者尝试创建一个存储库,以某种方式将所有这 3 个表都放在一个好名称中。

标签: c# .net asp.net asp.net-mvc repository-pattern


【解决方案1】:

存储库模式是一种糟糕的设计模式。我与许多旧的 .Net 项目合作,这种模式通常会导致可以避免的“分布式事务”、“部分回滚”和“连接池耗尽”错误。问题是该模式试图在内部处理连接和事务,但这些应该在控制器层处理。 EntityFramework 也已经抽象了很多逻辑。我建议改用服务模式来重用共享代码。

【讨论】:

    【解决方案2】:

    在使用存储库模式时我做错了一件事 - 就像您一样,我认为该表与存储库 1:1 相关。当我们应用领域驱动设计中的一些规则时 - 对存储库进行分组问题通常会消失。

    存储库应该是每个Aggregate root 而不是表。这意味着 - 如果实体不应该单独存在(即 - 如果你有一个 Registrant 特别参与 Registration) - 它只是一个实体,它不需要存储库,它应该被更新/创建/检索通过它所属的聚合根的存储库。

    当然 - 在许多情况下,这种减少存储库数量的技术(实际上 - 它更像是一种构建域模型的技术)无法应用,因为每个实体都应该是聚合根(这高度依赖于您的域名,我只能提供盲目猜测)。在您的示例中 - License 似乎是一个聚合根,因为您需要能够在没有 Registration 实体的上下文的情况下检查它们。

    但这并不限制我们使用级联存储库(Registration 存储库可以在需要时引用 License 存储库)。这并不限制我们直接从Registration 对象引用License 存储库(最好通过IoC)。

    尽量不要通过技术提供的复杂性或误解来推动您的设计。仅仅因为您不想构建 2 个存储库而在 ServiceX 中对存储库进行分组并不是一个好主意。

    最好给它一个合适的名字 - RegistrationService

    但一般应避免使用服务 - 它们通常是导致anemic domain model 的原因。

    编辑:
    开始使用 IoC。它确实减轻了注入依赖项的痛苦。
    而不是写:

    var registrationService = new RegistrationService(new RegistrationRepository(),  
          new LicenseRepository(), new GodOnlyKnowsWhatElseThatServiceNeeds());
    

    你会写:

    var registrationService = IoC.Resolve<IRegistrationService>();
    

    附:使用所谓的common service locator 会更好,但这只是一个示例。

    【讨论】:

    • 哈哈哈...我正在提供使用服务定位器的建议。看到我过去有多愚蠢总是很高兴。
    • @Arnis 听起来你的观点好像变了——我很感兴趣你现在会如何以不同的方式回答这个问题?
    • @ngm 自从我回答这个问题以来发生了很多变化。我仍然同意聚合根应该绘制事务边界(作为一个整体保存),但我对使用存储库模式抽象持久性不太乐观。最近 - 我只是直接使用 ORM,因为像急切/延迟加载管理这样的东西太尴尬了。专注于开发丰富的领域模型而不是专注于抽象持久性更有益。
    • @Developer 不,不完全是。他们仍然应该坚持无知。您从外部检索聚合根并在其上调用完成该工作的方法。我的域模型有零引用,只有标准的 .net 框架。要实现这一点,您必须拥有足够智能的丰富领域模型和工具(NHibernate 可以做到这一点)。
    • 相反。获取多个聚合通常意味着您可以使用 PK 或索引。比利用 EF 生成的关系快得多。根聚合越小,就越容易获得性能。
    【解决方案3】:

    Here is an example 使用 FluentNHibernate 的通用存储库实现。它能够持久化您为其编写映射器的任何类。它甚至能够根据映射器类生成您的数据库。

    【讨论】:

      【解决方案4】:

      我建议你看看Sharp Architecture。他们建议每个实体使用一个存储库。我目前在我的项目中使用它,并且对结果感到满意。

      【讨论】:

      • 我会在这里给 +1,但他表示 DI 或 IoC 容器不是一个选择(当然这些不是 Sharp Arch 的唯一好处)。我猜他正在处理很多现有代码。
      • 什么是实体?那是整个数据库吗?或者那是一个数据库表? DI 或 IoC 容器目前不是一个选项,因为我只是不想在我同时学习的其他 10 件事之上学习它。它们是我将在我的网站的下一个版本或我的下一个项目中研究的东西。尽管我不确定它是否会是这个网站,但它似乎希望你使用 nhirbrate,而我目前正在使用 linq to sql。
      【解决方案5】:

      我有这个作为我的存储库类,是的,我在表/区域存储库中进行了扩展,但有时我仍然必须打破 DRY。

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
      
      namespace MvcRepository
      {
          public class Repository<T> : IRepository<T> where T : class
          {
              protected System.Data.Linq.DataContext _dataContextFactory;
      
              public IQueryable<T> All()
              {
                  return GetTable.AsQueryable();
              }
      
              public IQueryable<T> FindAll(Func<T, bool> exp)
              {
                  return GetTable.Where<T>(exp).AsQueryable();
              }
      
              public T Single(Func<T, bool> exp)
              {
                  return GetTable.Single(exp);
              }
      
              public virtual void MarkForDeletion(T entity)
              {
                  _dataContextFactory.GetTable<T>().DeleteOnSubmit(entity);
              }
      
              public virtual T CreateInstance()
              {
                  T entity = Activator.CreateInstance<T>();
                  GetTable.InsertOnSubmit(entity);
                  return entity;
              }
      
              public void SaveAll()
              {
                  _dataContextFactory.SubmitChanges();
              }
      
              public Repository(System.Data.Linq.DataContext dataContextFactory)
              {
                  _dataContextFactory = dataContextFactory;
              }
      
              public System.Data.Linq.Table<T> GetTable
              {
                  get { return _dataContextFactory.GetTable<T>(); }
              }
      
          }
      }
      

      编辑

      public class AdminRepository<T> : Repository<T> where T: class
      {
          static AdminDataContext dc = new AdminDataContext(System.Configuration.ConfigurationManager.ConnectionStrings["MY_ConnectionString"].ConnectionString);
      
          public AdminRepository()
              : base( dc )
          {
          }
      

      我还有一个使用 Linq2SQL.dbml 类创建的数据上下文。

      所以现在我有一个标准存储库,实现了 All 和 Find 等标准调用,并且在我的 AdminRepository 中我有特定的调用。

      虽然我不认为,但没有回答 DRY 的问题。

      【讨论】:

      • 什么“存储库”?和 CreateInstance 一样吗?不知道你所拥有的一切都在做什么。
      • 它是通用的。基本上,您需要为您的(区域)有一个特定的存储库。查看上面对我的 AdminRepository 进行的编辑。
      【解决方案6】:

      我正在做的是我有一个抽象基类,定义如下:

      public abstract class ReadOnlyRepository<T,V>
      {
           V Find(T lookupKey);
      }
      
      public abstract class InsertRepository<T>
      {
           void Add(T entityToSave);
      }
      
      public abstract class UpdateRepository<T,V>
      {
           V Update(T entityToUpdate);
      }
      
      public abstract class DeleteRepository<T>
      {
           void Delete(T entityToDelete);
      }
      

      然后,您可以从抽象基类派生您的存储库并扩展您的单个存储库,只要通用参数不同,例如;

      public class RegistrationRepository: ReadOnlyRepository<int, IRegistrationItem>,
                                           ReadOnlyRepository<string, IRegistrationItem> 
      

      等等……

      我需要单独的存储库,因为我们确实对某些存储库有限制,这为我们提供了最大的灵活性。希望这会有所帮助。

      【讨论】:

      • 所以您尝试创建一个通用存储库来处理这一切?
      • 那么,Update 方法的真正含义是什么。就像你喜欢这个 V 更新一样,它传入了一个 T entitytoUpdate,但实际上没有代码更新它或者是否存在?
      • Yes.. update 方法中会有代码,因为您将编写一个从通用存储库继承的类。实现代码可以是 Linq2SQL 或 ADO.NET 或任何您选择的数据访问实现技术
      【解决方案7】:

      我已经开始解决这个问题的一件事是实际开发包装 N 个存储库的服务。希望您的 DI 或 IoC 框架可以帮助简化这一过程。

      public class ServiceImpl {
          public ServiceImpl(IRepo1 repo1, IRepo2 repo2...) { }
      }
      

      这有意义吗?另外,我明白,说到这个庄园里的服务可能会也可能不会真正符合 DDD 原则,我这样做只是因为它似乎有效。

      【讨论】:

      • 不,这没有多大意义。我目前不使用 DI 或 IoC 框架,因为我的盘子已经足够了。
      • 如果你可以用 new() 实例化你的 repos,你可以试试这个... public ServiceImpl() : this(new Repo1, new Repo2...) {} 作为附加服务中的构造函数。
      • 依赖注入?我已经这样做了,但仍然不确定您的代码是什么以及它解决了什么问题。
      • 我专门针对存储库的合并。哪个代码没有意义?如果您使用的是 DI,那么答案中的代码将在您的 DI 框架将注入那些 IRepo 服务的意义上起作用,在注释代码中,它基本上只是一个小工作来做 DI(基本上你的无参数构造函数“注入”将这些依赖项添加到您的 ServiceImpl 中)。
      • 我已经在使用 DI,所以我可以更好地进行单元测试。我的问题是,如果您将服务层和 repos 设置为详细的名称。然后,如果您需要在其他任何地方使用它们,它会看起来很奇怪,例如在其他类中调用 RegRepo 的 RegistrationService 层,例如 ProfileClass。所以我没有从你的例子中看到你在做什么。就像如果您在同一个服务层中开始有太多的 Repos,那么您将拥有如此不同的业务逻辑和验证逻辑。由于在服务层中,您通常会放入验证逻辑。这么多我需要更多...
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-17
      • 1970-01-01
      • 2014-02-11
      • 1970-01-01
      • 2013-03-31
      • 2022-10-18
      相关资源
      最近更新 更多