【问题标题】:Repository Methods vs. Extending IQueryable存储库方法与扩展 IQueryable
【发布时间】:2009-09-13 01:13:20
【问题描述】:

我有封装对域模型的数据访问的存储库(例如 ContactRepository、UserRepository 等)。

当我查看搜索数据时,例如

  • 查找其名字的联系人 以 XYZ 开头
  • 生日在之后的联系人 1960

    (等),

我开始实现诸如 FirstNameStartsWith(string prefix)YoungerThanBirthYear(int year) 之类的存储库方法,基本上遵循了许多示例。

然后我遇到了一个问题 - 如果我必须合并多个搜索怎么办?我的每个存储库搜索方法,例如上面的,只返回一组有限的实际域对象。为了寻找更好的方法,我开始在 IQueryable 上编写扩展方法,例如这个:

public static IQueryable<Contact> FirstNameStartsWith(
               this IQueryable<Contact> contacts, String prefix)
{
    return contacts.Where(
        contact => contact.FirstName.StartsWith(prefix));
}        

现在我可以做一些事情了

ContactRepository.GetAll().FirstNameStartsWith("tex").YoungerThanBirthYear(1960);

但是,我发现自己在编写扩展方法(并且到处发明诸如 ContactsQueryableExtensions 之类的疯狂类,并且由于将所有内容都放在适当的存储库中,我失去了“良好的分组”。

这真的是这样做的方法,还是有更好的方法来实现相同的目标?

【问题讨论】:

    标签: c# .net extension-methods repository-pattern iqueryable


    【解决方案1】:

    在开始我目前的工作之后,我最近一直在思考这个问题。我习惯了存储库,他们按照您的建议仅使用裸骨存储库来完成完整的 IQueryable 路径。

    我觉得 repo 模式是合理的,并且在描述您希望如何处理应用程序域中的数据方面做得很有效。但是,您所描述的问题肯定会发生。它变得凌乱、快速,超出了一个简单的应用程序。

    是否有办法重新思考为什么要以多种方式要求数据?如果不是,我真的觉得混合方法是最好的方法。为您重用的东西创建 repo 方法。实际上它有意义的东西。干燥等等。但是那些一次性?为什么不利用 IQueryable 和你可以用它做的性感事情呢?正如您所说,为此创建一种方法很愚蠢,但这并不意味着您不需要数据。 DRY 并不真正适用,不是吗?

    要做好这件事需要纪律,但我真的认为这是一条合适的道路。

    【讨论】:

      【解决方案2】:

      @Alex - 我知道这是一个老问题,但我要做的就是让存储库只做非常简单的事情。这意味着,获取表或视图的所有记录。

      然后,在 SERVICES 层(您使用的是 n 层解决方案,对吗?:))我将在那里处理所有“特殊”查询内容。

      好的,示例时间。

      存储层

      ContactRepository.cs
      
      public IQueryable<Contact> GetContacts()
      {
          return (from q in SqlContext.Contacts
                  select q).AsQueryable();
      }
      

      漂亮而简单。 SqlContext 是您的 EF Context .. 的实例,上面有一个名为 EntityContacts .. 这基本上是您的 sql Contacts 类。

      这意味着,该方法基本上是在做:SELECT * FROM CONTACTS ... 但它并没有通过该查询访问数据库 .. 它现在只是一个查询。

      好的 .. 下一层 .. KICK ...我们开始(Inception 有人吗?)

      服务层

      ContactService.cs
      
      public  ICollection<Contact> FindContacts(string name)
      {
          return FindContacts(name, null)
      }
      
      public ICollection<Contact> FindContacts(string name, int? year)
      {
         IQueryable<Contact> query = _contactRepository.GetContacts();
         
         if (!string.IsNullOrEmpty(name))
         {
             query = from q in query
                     where q.FirstName.StartsWith(name)
                     select q;
         }
      
         if (int.HasValue)
         {
             query = from q in query
                     where q.Birthday.Year <= year.Value
                     select q);
          }
      
          return (from q in query
                  select q).ToList();
      }
      

      完成。

      让我们回顾一下。首先,我们从一个简单的“Get all from contacts”查询开始。现在,如果我们提供了姓名,让我们添加一个过滤器来按姓名过滤所有联系人。接下来,如果我们提供了年份,那么我们按年份过滤生日。等等。最后,我们点击数据库(使用这个修改后的查询),看看我们得到了什么结果。

      注意事项:-

      • 为了简单起见,我省略了任何依赖注入。强烈推荐。
      • 这都是伪代码。未经测试(针对编译器),但你明白了....

      外卖要点

      • 服务层处理所有智能。在那里,您可以决定需要哪些数据。
      • 存储库是简单的 SELECT * FROM TABLE 或简单的 INSERT/UPDATE 到 TABLE。

      祝你好运:)

      【讨论】:

      • 我认为值得一提的是,这真的只能有效地工作,如果我错了,请纠正我,如果您使用支持延迟执行的 DAL,例如根据您的示例 Linq To Sql。否则,您将从数据存储中检索到大量可能不会被使用的数据。现在,如果您知道用户将以多种不同的方式使用相同的数据集,这最终可能没问题,但如果他们只运行一次此查询,然后检索完全不相关的数据,此设计将导致可提及的开销。
      • 我们都做过这个hack。对于每个额外的过滤器,我们添加一个“可选”参数。我认为作者的原始示例更简洁,更易于使用(和重用)。
      • @joshlrogers :伙计 - 哇哦不正确!你知道 IQueryable 是什么吗?它不会击中数据库。这是一个查询。所以我并没有真正做 SELECT * FROM XXX。我通过在需要时添加额外的 where 子句来扩展该查询。所以最终的 sql 实际上是一个 SELECT * FROM XXX WHERE blah(如果用户提供了 optional 名称和/或 optional 年份参数*。所以除非我误解了你。 .小心你说的话:(
      • @Jerod Houghtelling:黑客?这是怎么回事?使用作者的原始示例,他们将拥有一个非常复杂的存储库(正如他/她所建议的那样).. 然后在服务层中进行排序复制。如果你问我,不是很干。想要我的答案更深入的例子吗?查看这篇优秀的博文:huyrua.wordpress.com/2010/07/13/…
      • 现在明白了 :) 是的 - 同意。但我并不担心,因为这是一个 EF 问题 :)
      【解决方案3】:

      我意识到这已经过时了,但我最近一直在处理同样的问题,我得出了与 Chad 相同的结论:只要稍加注意,扩展方法和存储库方法的混合似乎效果最好。

      我在(实体框架)应用程序中一直遵循的一些一般规则:

      订购查询

      如果该方法仅用于订购,我更喜欢编写在IQueryable&lt;T&gt;IOrderedQueryable&lt;T&gt; 上运行的扩展方法(以利用底层提供者。)例如

      public static IOrderedQueryable<TermRegistration> ThenByStudentName(
          this IOrderedQueryable<TermRegistration> query)
      {
          return query
              .ThenBy(reg => reg.Student.FamilyName)
              .ThenBy(reg => reg.Student.GivenName);
      }
      

      现在我可以在我的存储库类中根据需要使用ThenByStudentName()

      返回单个实例的查询

      如果该方法涉及通过原始参数进行查询,通常需要ObjectContext,并且不容易制作static。我将这些方法留在我的存储库中,例如

      public Student GetById(int id)
      {
          // Calls context.ObjectSet<T>().SingleOrDefault(predicate) 
          // on my generic EntityRepository<T> class 
          return SingleOrDefault(student => student.Active && student.Id == id);
      }
      

      但是,如果该方法涉及使用其navigation properties 查询EntityObject,则通常可以很容易地将其制成static,并作为扩展方法实现。 例如

      public static TermRegistration GetLatestRegistration(this Student student)
      {
          return student.TermRegistrations.AsQueryable()
              .OrderByTerm()
              .FirstOrDefault();
      }
      

      现在我可以方便地编写someStudent.GetLatestRegistration(),而不需要当前范围内的存储库实例。

      查询返回集合

      如果该方法返回一些IEnumerableICollectionIList,那么我希望尽可能将其设为static,并将其保留在存储库中即使它使用导航属性。 em> 例如

      public static IList<TermRegistration> GetByTerm(Term term, bool ordered)
      {
          var termReg = term.TermRegistrations;
          return (ordered)
              ? termReg.AsQueryable().OrderByStudentName().ToList()
              : termReg.ToList();
      }
      

      这是因为我的 GetAll() 方法已经存在于存储库中,它有助于避免扩展方法的混乱。

      不将这些“集合获取器”实现为扩展方法的另一个原因是它们需要更详细的命名才能有意义,因为没有隐含返回类型。例如,最后一个示例将变为 GetTermRegistrationsByTerm(this Term term)

      我希望这会有所帮助!

      【讨论】:

      • 有意义的命名,也就是 通用语言 是服务层的关键目标和好处。 GetRegistrationsByTerm() 就是一个很好的例子。
      【解决方案4】:

      六年后,我确信@Alex 已经解决了他的问题,但在阅读了接受的答案后,我想加两分钱。

      存储库中扩展IQueryable 集合的一般目的是提供灵活性并使其消费者能够自定义数据检索。亚历克斯已经做的很好。

      服务层的主要作用是遵守关注点分离原则并处理与业务功能相关的命令逻辑

      在现实世界的应用程序中,查询逻辑通常不需要扩展存储库本身提供的检索机制(例如值更改、类型转换)。

      考虑以下两种情况:

      IQueryable<Vehicle> Vehicles { get; }
      
      // raw data
      public static IQueryable<Vehicle> OwnedBy(this IQueryable<Vehicle> query, int ownerId)
      {
          return query.Where(v => v.OwnerId == ownerId);
      }
      
      // business purpose
      public static IQueryable<Vehicle> UsedThisYear(this IQueryable<Vehicle> query)
      {
          return query.Where(v => v.LastUsed.Year == DateTime.Now.Year);
      }
      

      这两种方法都是简单的查询扩展,但它们有不同的作用。第一个是一个简单的过滤器,而第二个意味着业务需求(例如维护或计费)。在一个简单的应用程序中,可能会在存储库中实现它们。在更理想化的系统中,UsedThisYear 最适合服务层(甚至可以作为普通实例方法实现),它还可以更好地促进 CQRS 分离命令的策略 em> 和查询

      主要考虑因素是 (a) 存储库的主要目的和 (b) 您希望在多大程度上遵守 CQRSDDD 理念。

      【讨论】:

        猜你喜欢
        • 2014-02-15
        • 1970-01-01
        • 2010-10-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多