设计良好的查询命令和/或规范

本文关键字:命令 查询 | 更新日期: 2023-09-27 17:54:26

我一直在寻找一个很好的解决方案来解决典型的存储库模式所带来的问题(不断增长的专门查询方法列表等)。见:http://ayende.com/blog/3955/repository-is-the-new-singleton) .

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

(注意:我在command模式中使用术语"命令",也称为查询对象)。我不是在说命令/查询分离中的命令,在那里查询和命令(更新,删除,插入)之间有区别

所以我正在寻找封装整个查询的替代方案,但仍然足够灵活,您不只是将意大利面条存储库交换为命令类的爆炸。

例如,我使用过Linqspecs,虽然我发现能够为选择标准分配有意义的名称有一些价值,但这还不够。也许我正在寻找一种混合的解决方案,结合多种方法。

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

等待期一过,我就会给你赏金。所以请让你的解决方案值得赏金,有好的解释,我会选择最好的解决方案,并为亚军投票。

注意:我正在寻找的东西是基于ORM。不一定要显式地使用EF或nHibernate,但这些是最常见的,也最适合。如果它可以很容易地适应其他ORM,那将是一个额外的好处。如果能兼容Linq就好了。

更新:我真的很惊讶这里没有很多好的建议。看起来人们要么完全是CQRS,要么完全是Repository阵营。我的大多数应用程序都不够复杂,不足以保证使用CQRS(大多数CQRS倡导者都很乐意说你不应该使用它)。

更新:这里似乎有点混乱。我不是在寻找一种新的数据访问技术,而是在业务和数据之间设计得相当好的接口。

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

设计良好的查询命令和/或规范

免责声明:由于还没有任何好的答案,我决定从我不久前读到的一篇很棒的博客文章中摘录一部分,几乎是逐字复制的。你可以在这里找到完整的博客文章。这里是:


可以定义以下两个接口:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

IQuery<TResult>指定了一条消息,该消息使用TResult泛型返回的数据定义了一个特定的查询。使用前面定义的接口,我们可以像这样定义查询消息:

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>上的泛型类型约束将无法将FindUsersBySearchTextQuery映射到User[]

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

我们可以用一个额外的抽象层来解决必须注入太多IQueryHandlers的问题。我们在消费者和查询处理程序之间创建一个中介:

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

IQueryProcessor是一个具有一个泛型方法的非泛型接口。正如您在接口定义中看到的,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#的类型推断,编译器能够确定泛型类型,这使我们不必显式地声明类型。Process方法的返回类型也是已知的。

找到正确的IQueryHandler现在是IQueryProcessor实现的责任。这需要一些动态类型,并且可以选择使用依赖注入框架,并且只需几行代码就可以完成:

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类基于所提供的查询实例的类型构造一个特定的IQueryHandler<TQuery, TResult>类型。此类型用于请求提供的容器类获取该类型的实例。不幸的是,我们需要使用反射来调用Handle方法(在本例中使用c# 4.0动态关键字),因为此时不可能强制转换处理程序实例,因为通用的TQuery参数在编译时不可用。但是,除非重命名Handle方法或获得其他参数,否则该调用永远不会失败,如果您想要失败,为该类编写单元测试非常容易。使用反射会有轻微的下降,但是没有什么好担心的。


回答你的一个问题:

所以我正在寻找封装整个查询的替代方案,但是仍然足够灵活,你不只是交换意大利面大量命令类的存储库。

使用这种设计的结果是系统中将会有很多小类,但是拥有很多小的/集中的类(具有清晰的名称)是一件好事。这种方法显然比在存储库中对同一方法使用不同参数的许多重载要好得多,因为您可以将这些重载分组到一个查询类中。因此,您在存储库中获得的查询类仍然比方法少得多。

我处理这个问题的方法实际上很简单,并且与ORM无关。我对存储库的看法是:存储库的工作是为应用程序提供上下文所需的模型,所以应用程序只是向repo询问它想要什么,但不告诉它如何获得它。

我为存储库方法提供了一个Criteria(是的,DDD风格),repo将使用它来创建查询(或任何需要的东西——它可能是一个web服务请求)。连接和组是如何构建where子句的细节,而不是什么,标准应该只是构建where子句的基础。

Model =应用程序需要的最终对象或数据结构。

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)。存储库实现应该知道如何将Criteria与底层存储或DAO一起使用。

我不知道你的领域和模型要求,但如果最好的方法是应用程序构建查询本身,那将是奇怪的。模型变化如此之大,以至于你无法定义某种稳定的东西?

这个解决方案显然需要一些额外的代码,但它没有将其余部分耦合到ORM或您用来访问存储的任何东西。存储库做它的工作作为一个门面,在我看来,它是干净的,"标准翻译"代码是可重用的

我做了这个,支持这个,撤消这个。

主要的问题是:不管你怎么做,增加的抽象并不能让你获得独立性。根据定义,它会泄漏。从本质上讲,你发明了一个完整的层,只是为了让你的代码看起来可爱……但是它并没有减少维护,提高可读性,或者让你获得任何类型的模型不可知论。

有趣的是你在回答Olivier的回答时回答了你自己的问题:"这实际上是复制了Linq的功能,却没有Linq的所有好处"。

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

可以使用流畅的接口。基本思想是,类的方法在执行某些操作后返回这个类的当前实例。这允许你链接方法调用。

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

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();

只能新建Query实例。其他类有一个受保护的构造函数。层次结构的意义在于"禁用"方法。例如,GroupBy方法返回GroupedQuery,它是Query的基类,并且没有Where方法(where方法在Query中声明)。因此不可能在GroupBy之后调用Where

然而,它并不完美。使用这个类层次结构,您可以依次隐藏成员,但不能显示新成员。因此,HavingGroupBy之前被调用时会抛出异常。

注意可以多次调用Where。这将在现有条件中添加具有AND的新条件。这使得从单个条件以编程方式构造过滤器变得更加容易。对于Having也是一样的。

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


流畅的接口非常灵活,不需要你用不同的参数组合创建大量的方法重载。我的示例使用字符串,但是这种方法可以扩展到其他类型。您还可以为特殊情况声明预定义的方法或接受自定义类型的方法。您还可以添加ExecuteReaderExceuteScalar<T>之类的方法。这将允许您定义如下查询

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

即使以这种方式构造的SQL命令也可以有命令参数,从而避免SQL注入问题,同时允许命令被数据库服务器缓存。这不是O/r映射器的替代品,但在使用简单的字符串连接创建命令的情况下可以提供帮助。