精心设计的查询命令和/或规范

时间:2013-01-20 00:01:23

标签: c# repository-pattern command-pattern specification-pattern

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

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

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

所以我正在寻找封装整个查询的替代方案,但仍然足够灵活,以至于您不仅仅需要交换spaghetti Repositories来扩展命令类。

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

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

一旦等待期结束,我就会在此提供赏金。因此,请为您的解决方案提供有价值的解决方案,并提供良好的解释,我将选择最佳解决方案,并为选手提供支持。

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

更新:我真的很惊讶这里没有很多好的建议。似乎人们完全是CQRS,或者他们完全在资源库中。我的大多数应用程序都不够复杂,无法保证CQRS(大多数CQRS倡导者都会说你不应该使用它)。

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

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

4 个答案:

答案 0 :(得分:85)

免责声明:由于还没有很好的答案,我决定在我刚读过的一篇很棒的博客文章中发帖,几乎是逐字复制的。您可以找到完整的博文here。所以这就是:


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

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(以及该接口的所有其他使用者)进行更改。

在我们的代码中指定或注入IQuery<TResult>时,IQueryHandlers接口为我们提供了编译时支持。当我们将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现在取决于可以处理我们所有查询的IQueryProcessorUserController的{​​{1}}方法调用传入初始化查询对象的SearchUsers方法。由于IQueryProcessor.Process实现了FindUsersBySearchTextQuery接口,因此我们可以将其传递给通用IQuery<User[]>方法。由于C#类型推断,编译器能够确定泛型类型,这节省了我们必须明确说明类型。 Execute<TResult>(IQuery<TResult> query)方法的返回类型也是已知的。

现在,Process的实施有责任找到合适的IQueryProcessor。这需要一些动态类型,并且可选地使用依赖注入框架,并且只需几行代码即可完成:

IQueryHandler

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>方法(在这种情况下使用C#4.0 dymamic关键字),因为此时无法强制转换处理程序实例,因为通用Handle参数是在编译时不可用。但是,除非重命名TQuery方法或获取其他参数,否则此调用永远不会失败,如果您愿意,可以很容易地为此类编写单元测试。使用反射会略微下降,但没什么可担心的。


回答您的一个问题:

  

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

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

答案 1 :(得分:4)

我处理这个问题的方式实际上是简单的,与ORM无关。我对存储库的看法是这样的:存储库的工作是为应用程序提供上下文所需的模型,因此应用程序只是要求回购提供它想要的但不告诉它如何来获得它。

我为存储库方法提供了Criteria(是的,DDD样式),repo将使用它来创建查询(或者任何需要的东西 - 它可能是webservice请求)。连接和组imho是详细信息,而不是什么和标准应该只是构建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或您用于访问存储的任何内容耦合。存储库的作用是作为一个外观,IMO是干净的,“标准转换”代码是可重用的

答案 2 :(得分:2)

我已经完成了这项工作,支持这一点并撤消了这一点。

主要问题在于:无论你如何做,增加的抽象都不会让你获得独立性。根据定义它会泄漏。从本质上讲,您只是为了让您的代码看起来很可爱而发明了整个图层......但它不会减少维护,提高可读性或获得任何类型的模型不可知论。

有趣的是,你回答了自己的问题以回应Olivier的回答:“这实际上是复制了Linq的功能而没有你从Linq获得的所有好处。”

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

答案 3 :(得分:1)

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

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

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方法在{{1}中声明})。因此,无法在Query之后调用Where

然而,它并不完美。使用此类层次结构,您可以连续隐藏成员,但不能显示新成员。因此GroupByHaving之前调用时会引发异常。

请注意,可以多次调用GroupBy。这为现有条件添加了Where的新条件。这使得从单一条件以编程方式构造过滤器变得更加容易。 AND也可以这样做。

接受字段列表的方法具有参数Having。它允许您传递单个字段名称或字符串数​​组。


Fluent界面非常灵活,不需要您使用不同的参数组合创建大量方法重载。我的示例使用字符串,但该方法可以扩展到其他类型。您还可以为接受自定义类型的特殊情况或方法声明预定义方法。您还可以添加params string[] fieldsExecuteReader等方法。这将允许您定义像这样的查询

ExceuteScalar<T>

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