精心devise的查询命令和/或规格

我一直在寻找一个很好的解决scheme来解决典型的Repository模式(针对特定查询的不断增长的方法列表,请参阅http://ayende.com/blog/3955/repository-是新的单身人士 )。

我非常喜欢使用Command查询的想法,特别是通过使用Specification模式。 然而,我的规范问题是它只涉及到简单select的标准(基本上是where子句),并没有处理查询的其他问题,如连接,分组,子集select或投影等。基本上,所有额外的许多查询必须经过以获得正确的数据集。

(注意:我在命令模式中使用了“命令”这个术语,也称为查询对象。我不是在命令/查询分离中讨论命令,在查询和命令之间有区别(更新,删除,插))

所以我正在寻找封装整个查询的替代scheme,但是仍然足够灵活,以至于您不只是换取意大利面条仓库来获取命令类的爆炸式增长。

我已经使用了Linqspecs,虽然我发现能够为select标准分配有意义的名称是有价值的,但这还不够。 也许我正在寻求结合了多种方法的混合解决scheme。

我正在寻找其他人可能已经开发的解决这个问题的解决scheme,或者解决不同的问题,但是仍然满足这些要求。 在链接的文章中,Ayendebuild议直接使用nHibernate上下文,但是我觉得这在很大程度上使业务层复杂化,因为它现在还必须包含查询信息。

等待一段时间后,我会提供一个赏金。 所以,请让您的解决scheme赏金值得,好的解释,我会select最好的解决scheme,并upvote亚军。

注:我正在寻找一些基于ORM的东西。 显然不一定是EF或nHibernate,但这些是最常见的,并且最适合。 如果它可以很容易地适应其他ORM的,这将是一个奖金。 Linq兼容也不错。

更新:我真的很惊讶,这里没有很多好的build议。 看起来好像人们完全是CQRS,或者他们完全在版本库里。 我的大多数应用程序都不够复杂,不能保证CQRS(大多数CQRS提倡者都表示不应该使用它)。

更新:这里似乎有点混乱。 我不是在寻找新的数据访问技术,而是在业务和数据之间devise合理的界面。

理想情况下,我正在寻找的是查询对象,规范模式和存储库之间的某种交叉。 正如我上面所说,规范模式只处理where子句方面,而不是查询的其他方面,如连接,子select等。存储库处理整个查询,但一段时间后失控。 查询对象也处理整个查询,但我不想简单地用查询对象的爆炸replace存储库。

免责声明:由于目前还没有什么好的答案,我决定从我刚才阅读的一篇很棒的博客文章中发表一篇文章,几乎是逐字的复制。 你可以在这里find完整的博客文章。 所以这里是:


我们可以定义以下两个接口:

public interface IQuery<TResult> { } public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { TResult Handle(TQuery query); } 

IQuery<TResult>指定一个消息,用TResultgenericstypes返回的数据定义特定的查询。 通过先前定义的接口,我们可以像这样定义一个查询消息:

 public class FindUsersBySearchTextQuery : IQuery<User[]> { public string SearchText { get; set; } public bool IncludeInactiveUsers { get; set; } } 

该类使用两个参数定义一个查询操作,这将导致一个User对象数组。 处理这个消息的类可以定义如下:

 public class FindUsersBySearchTextQueryHandler : IQueryHandler<FindUsersBySearchTextQuery, User[]> { private readonly NorthwindUnitOfWork db; public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db) { this.db = db; } public User[] Handle(FindUsersBySearchTextQuery query) { return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray(); } } 

现在我们可以让消费者依赖通用的IQueryHandler接口:

 public class UserController : Controller { IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler; public UserController( IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler) { this.findUsersBySearchTextHandler = findUsersBySearchTextHandler; } public View SearchUsers(string searchString) { var query = new FindUsersBySearchTextQuery { SearchText = searchString, IncludeInactiveUsers = false }; User[] users = this.findUsersBySearchTextHandler.Handle(query); return View(users); } } 

这个模型立刻给了我们很大的灵活性,因为我们现在可以决定将什么注入到UserController 。 我们可以注入一个完全不同的实现,或者包装真实实现的实现,而不必对UserController (以及该接口的所有其他消费者)进行更改。

在我们的代码中指定或注入IQueryHandlers时, IQuery<TResult>接口为我们提供编译时支持。 当我们改变FindUsersBySearchTextQuery返回UserInfo[]来代替(通过实现IQuery<UserInfo[]> ), UserController将无法编译,因为IQueryHandler<TQuery, TResult>的通用types约束将无法将FindUsersBySearchTextQuery映射到User[]

然而,将IQueryHandler接口注入到消费者中,有一些不太明显的问题仍然需要解决。 我们的消费者的依赖关系的数量可能会变得太大,并可能导致构造函数过度注入 – 当一个构造函数需要太多的参数。 类执行的查询次数可能会频繁变化,这将需要不断修改构造函数参数的数量。

我们可以解决一个额外的抽象层注入太多IQueryHandlers的问题。 我们创build一个介于消费者和查询处理程序之间的介体:

 public interface IQueryProcessor { TResult Process<TResult>(IQuery<TResult> query); } 

IQueryProcessor是一个通用方法的非generics接口。 正如你在接口定义中看到的那样, IQueryProcessor依赖于IQuery<TResult>接口。 这允许我们在依赖于IQueryProcessor消费者中拥有编译时支持。 我们重写UserController以使用新的IQueryProcessor

 public class UserController : Controller { private IQueryProcessor queryProcessor; public UserController(IQueryProcessor queryProcessor) { this.queryProcessor = queryProcessor; } public View SearchUsers(string searchString) { var query = new FindUsersBySearchTextQuery { SearchText = searchString, IncludeInactiveUsers = false }; // Note how we omit the generic type argument, // but still have type safety. User[] users = this.queryProcessor.Process(query); return this.View(users); } } 

UserController现在依赖于一个可以处理我们所有查询的IQueryProcessorUserControllerSearchUsers方法调用传入初始化查询对象的IQueryProcessor.Process方法。 由于FindUsersBySearchTextQuery实现了IQuery<User[]>接口,我们可以将它传递给通用的Execute<TResult>(IQuery<TResult> query)方法。 由于C#types推理,编译器能够确定genericstypes,这就省去了我们必须明确说明的types。 Process方法的返回types也是已知的。

现在, IQueryProcessor的实现负责查找正确的IQueryHandler 。 这需要一些dynamictypes,可选地使用dependency injection框架,并且只需要几行代码即可完成:

 sealed class QueryProcessor : IQueryProcessor { private readonly Container container; public QueryProcessor(Container container) { this.container = container; } [DebuggerStepThrough] public TResult Process<TResult>(IQuery<TResult> query) { var handlerType = typeof(IQueryHandler<,>) .MakeGenericType(query.GetType(), typeof(TResult)); dynamic handler = container.GetInstance(handlerType); return handler.Handle((dynamic)query); } } 

QueryProcessor类根据提供的查询实例的types构造特定的IQueryHandler<TQuery, TResult>types。 这个types用于请求提供的容器类来获取该types的实例。 不幸的是,我们需要使用reflection来调用Handle方法(在这种情况下,通过使用C#4.0 dymamic关键字),因为此时不可能TQuery处理程序实例,因为通用TQuery参数在编译时不可用。 但是,除非Handle方法被重命名或获取其他参数,否则这个调用永远不会失败,如果你愿意的话,为这个类编写一个unit testing是很容易的。 使用reflection会略微下降,但没有什么真正担心的。


回答您的一个担心:

所以我正在寻找封装整个查询的替代scheme,但是仍然足够灵活,以至于您不只是换取意大利面条仓库来获取命令类的爆炸式增长。

使用这种devise的结果是系统中会有很多小的类,但是有很多小的/重点的类(名字清晰)是一件好事。 这种方法显然要好得多,因为存储库中的同一个方法有不同的参数重载,你可以将它们分组在一个查询类中。 因此,与存储库中的方法相比,查询类仍然less得多。

我的处理方式实际上是简单的,ORM不可知论的。 我对存储库的看法是这样的:存储库的工作是为应用程序提供上下文所需的模型,所以应用程序只是回答它想要的东西 ,但并没有告诉它如何得到它。

我提供了一个Criteria(yes,DDD样式)的仓库方法,这个仓库将被repo用来创build查询(或者任何需要的 – 它可能是一个web服务请求)。 联合和组imho是如何的细节,而不是什么和一个标准应该只是build立一个where子句的基础。

模型=应用程序的最终对象或数据结构。

 public class MyCriteria { public Guid Id {get;set;} public string Name {get;set;} //etc } public interface Repository { MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria); } 

也许你可以直接使用ORM标准(Nhibernate)。 存储库实现应该知道如何将标准与底层存储或DAO一起使用。

我不知道你的域和模型的要求,但如果最好的方法是build立查询本身的应用程序将是奇怪的。 模型变化太大,你不能定义一些稳定的东西?

这个解决scheme显然需要一些额外的代码,但是它并没有将其余部分与ORM或者你用来访问存储的任何东西结合起来。 仓库的工作是作为一个门面,IMO是干净的,“标准翻译”代码是可重复使用的

我已经做到了这一点,支持这一点,并取消了这一点。

主要的问题是:不pipe你怎么做,增加的抽象不会让你独立。 它会根据定义泄漏。 从本质上说,你只是为了让代码看起来很可爱而创造出一个完整的图层,但是这不会减less维护,提高可读性或者获得任何types的模型不可知论。

有趣的部分是,你回答了你自己的问题,以回应Olivier的回应:“这实质上是复制了Linq的function,没有你从Linq获得的所有好处。

问问自己:怎么可能不是?

你可以使用stream畅的界面。 基本的想法是,一个类的方法在执行了一些操作之后返回这个非常类的当前实例。 这使您可以链接方法调用。

通过创build适当的类层次结构,可以创build可访问方法的逻辑stream。

 public class FinalQuery { protected string _table; protected string[] _selectFields; protected string _where; protected string[] _groupBy; protected string _having; protected string[] _orderByDescending; protected string[] _orderBy; protected FinalQuery() { } public override string ToString() { var sb = new StringBuilder("SELECT "); AppendFields(sb, _selectFields); sb.AppendLine(); sb.Append("FROM "); sb.Append("[").Append(_table).AppendLine("]"); if (_where != null) { sb.Append("WHERE").AppendLine(_where); } if (_groupBy != null) { sb.Append("GROUP BY "); AppendFields(sb, _groupBy); sb.AppendLine(); } if (_having != null) { sb.Append("HAVING").AppendLine(_having); } if (_orderBy != null) { sb.Append("ORDER BY "); AppendFields(sb, _orderBy); sb.AppendLine(); } else if (_orderByDescending != null) { sb.Append("ORDER BY "); AppendFields(sb, _orderByDescending); sb.Append(" DESC").AppendLine(); } return sb.ToString(); } private static void AppendFields(StringBuilder sb, string[] fields) { foreach (string field in fields) { sb.Append(field).Append(", "); } sb.Length -= 2; } } public class GroupedQuery : FinalQuery { protected GroupedQuery() { } public GroupedQuery Having(string condition) { if (_groupBy == null) { throw new InvalidOperationException("HAVING clause without GROUP BY clause"); } if (_having == null) { _having = " (" + condition + ")"; } else { _having += " AND (" + condition + ")"; } return this; } public FinalQuery OrderBy(params string[] fields) { _orderBy = fields; return this; } public FinalQuery OrderByDescending(params string[] fields) { _orderByDescending = fields; return this; } } public class Query : GroupedQuery { public Query(string table, params string[] selectFields) { _table = table; _selectFields = selectFields; } public Query Where(string condition) { if (_where == null) { _where = " (" + condition + ")"; } else { _where += " AND (" + condition + ")"; } return this; } public GroupedQuery GroupBy(params string[] fields) { _groupBy = fields; return this; } } 

你会这样称呼它

 string query = new Query("myTable", "name", "SUM(amount) AS total") .Where("name LIKE 'A%'") .GroupBy("name") .Having("COUNT(*) > 2") .OrderBy("name") .ToString(); 

您只能创build一个新的Query实例。 其他类有一个受保护的构造函数。 层次的要点是“禁用”方法。 例如, GroupBy方法返回一个GroupedQuery ,它是Query的基类,没有Where方法( Where方法在Query声明)。 因此无法在GroupBy之后调用Where

然而,这并不完美。 有了这个类层次结构,你可以先后隐藏成员,但不能显示新成员。 因此Having GroupBy之前调用它时引发exception。

请注意,有可能呼叫几次。 这为现有条件添加了AND条件。 这使得从单一条件编程filter变得更容易。 Having同样的可能。

接受字段列表的方法有一个参数params string[] fields 。 它允许您传递单个字段名称或string数​​组。


Fluent接口非常灵活,不需要用不同的参数组合来创build大量的方法重载。 我的例子工作与string,但是这种方法可以扩展到其他types。 您还可以为接受自定义types的特殊情况或方法声明预定义的方法。 您也可以添加像ExecuteReaderExceuteScalar<T> 。 这将允许你定义像这样的查询

 var reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true }) .Where(new CurrentMonthCondition()) .Where(new DivisionCondition{ DivisionType = DivisionType.Production}) .OrderBy(new StandardMonthlyReportSorting()) .ExecuteReader(); 

即使以这种方式构build的SQL命令也可以具有命令参数,从而避免SQL注入问题,同时允许数据库服务器caching命令。 这不是O / R映射器的替代品,但是可以帮助您在其他情况下使用简单string连接创build命令的情况。