使用多个具体实现处理存储库模式中的查询?

时间:2011-10-26 15:48:14

标签: linq generics repository

这更多是学术上的好奇心,但我正在努力弄清楚如何最好地完成以下任务。

想象一下你有一个Person对象

的情况
public class Person {
    public string Name {get;set;}
    public int Age {get;set;}
}

和从一些持久性存储中检索它们的存储库合同......

public class IPersonRepository {
    public IEnumerable<Person> Search(*** SOME_METHOD_SIGNATURE ***);
}

您的消费者应用程序确实不关心具体实现。它将从Unity / Ninject&amp; amp;中获取正确的具体实现。开始查询。

IPersonRespository repo = GetConcreteImplementationFromConfig();
repo.Search( ... );

我想知道的是你在这里使用的方法签名是什么?无论实现如何,都可以扩展。

选项1。

public IEnumerable<Person> Search(Expression<Func<Person, bool>> expression);

这很好,因为如果你使用的是LINQ Capable(例如EntityFramework)数据上下文,你可以直接将表达式传递给你的上下文。如果您的实现必须使用手工制作的存储过程/ sql / ADO.NET等,这个选项似乎就会失败......

选项2。

public IEnumerable<Person> Search(PersonSearch parameters);

public class PersonSearch {
    public int? Age {get;set;}
    public string FullName {get;set;}
    public string PartialName { get; set; }
}

这个似乎是最灵活的(在某种意义上它可以与Linq,Plain Old SQL一起使用。

但它只是“编写自己的查询语言”的臭味,因为你需要考虑消费者可能想要做的每一个可能的查询。例如 年龄&gt; = 18&amp;&amp;年龄&lt; = 65&amp;&amp;名字喜欢'%John%'

选项3。

还有其他选择吗?

3 个答案:

答案 0 :(得分:6)

  1. 您的声明“如果您的实现必须使用手工制作的存储过程/ sql / ADO.NET等,则选项1似乎会失败”是不正确的。通过使用ExpressionVisitor实现解释查询表达式,完全可以生成与选项2中的模型类似的模型。

  2. 如果要从LINQ IEnumerable<T>方法返回IQueryable<T>而不是Search,则应该采用一些机制来支持(1)分页,以及(2)排序

  3. 我真的不喜欢选项#2。它没有明确说明您要搜索的内容。例如如果年龄为null,这是否意味着您正在搜索具有空年龄的用户,或者您是否忽略年龄参数?如果同时指定了Name和PartialName怎么办?使用LINQ方法,您可以执行StartsWithContainsEquals等内容。

  4. 存储库模式实现应该抽象出数据访问逻辑,并公开面向业务的接口。通过使用通用接口(Expression<Func<Person,bool>>)直接访问存储库,您将失去一点点,因为接口不会向调用者传达意图。有几种方法可以做得更好。

    • 实现基于LINQ表达式的Specification Pattern会创建更强类型的查询。因此,不是通过Search(person => person.Age.HasValue && person.Age.Value > 18)查询成人,而是使用规范语法,如Search(new PersonIsAdultSpecification());,其中规范包装基础LINQ表达式,但公开面向业务的接口。
      • 就个人而言,我喜欢这种方法,但Ayende calls it 'architecting in the pit of doom',因为它很容易导致过度工程化。他的另一个建议是在扩展方法中包含这样的特定查询。我认为这可能同样可行,但我更喜欢使用强类型对象。
      • 最简单的方法是实际声明您将在IPersonRepository界面上执行的实际查询。因此,接口实际上会声明SearchForAdults()方法。
  5. 通常,无论何时查询数据库,都应该刻意尝试获取某些数据。每当您查询存储库时,您都应该刻意尝试获取满足特定业务约束的业务对象。为什么不使这个业务约束更明确?为任何服务定义良好的接口取决于该服务的使用者。没有通用的魔术子弹存储库接口,但您可以在特定应用程序的上下文中创建更好或更差的接口。我们的想法是记住您首先使用存储库模式的原因,那就是简化逻辑并创建更直观的访问点。

答案 1 :(得分:1)

我不确定在这种情况下是否存在“官方来源”这样的事情,但我确实领导了LINQ for Visual Basic的设计,所以这可能是件有用的。无论如何,这是我的看法:

您似乎提出的问题是,您希望能够将谓词表达式传输到后端,“无论[后端]实现如何”。但是,在我看来,您列出的两个选项实际上只是彼此的变体,因为在这两种情况下,后端必须能够理解传入的谓词规范。选项一恰好更通用和灵活比选项二(但翻译更痛苦)。选项二比选项2更受限制但是更简单的结构要处理(尽管在第二个选项中允许指定越来越复杂的查询,结构将不可避免地越来越像第一个选项中的结构)。

底线是你的问题没有好的答案。没有通用的表达式结构,您可以确保所有提供程序都支持,因此,如果确实想要一个适用于任何提供程序的Search方法,无论实现如何,如果提供者不支持表达式树,那么必须愿意回退到本地运行谓词。视情况而定,可能适用于您,也可能不适合您。

另一方面,如果(更有可能)您接受的可能提供商列表是有限的,您可能最好决定什么对您最重要(易用性/可表达性与易用性)实现)并选择你想要提供者的约束。对于消费者应用程序员来说,表达树肯定是许多方面中最好的,所以如果你认为你可以逃脱这种限制,那就去做吧。否则,某种更简单的自定义结构可能会减少您的痛苦和心痛。 (除非你有更多的额外时间和耐心,否则我不建议你自己翻译表达树。)

答案 2 :(得分:1)

关于存储库模式总是困扰我的一件事是它归结为分组,聚合计数等等的不灵活性。这就是为什么我决定也提供一个单独的接口(例如IExtendedRepository),它带有一个像这样的funcy方法签名:

TResult Aggregate<TResult>(Func<IQueryable<TEntity>, TResult> aggregatorFunc);

这种方法有点怪异,你可以用很多方式使用它:

var countSomething = repository.Aggregate(x => x.Where(y => y.SomeProperty).Count());

请注意(根据您的存储库实现),Count()调用将编译为SQL Count,因此它不是内存中的Count()。