【问题标题】:Well designed query commands and/or specifications精心设计的查询命令和/或规范
【发布时间】:2013-01-03 10:10:15
【问题描述】:

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

我真的很喜欢使用命令查询的想法,尤其是通过使用规范模式。但是,我对规范的问题是它只涉及简单选择的标准(基本上是 where 子句),而不涉及查询的其他问题,例如连接、分组、子集选择或投影等。基本上,许多查询必须经过所有额外的循环才能获得正确的数据集。

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

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

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

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

等待期结束后,我将为此提供赏金。因此,请让您的解决方案有价值,并提供良好的解释,我将选择最佳解决方案,并为亚军投票。

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

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

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

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

【问题讨论】:

  • 好问题。我也想看看比我建议有更多经验的人。目前,我正在开发一个代码库,其中通用存储库还包含 Command 对象或 Query 对象的重载,其结构类似于 Ayende 在他的博客中描述的内容。 PS:这可能也会引起programmers.SE的一些关注。
  • 如果您不介意对 LINQ 的依赖,为什么不直接使用公开 IQueryable 的存储库?一种常见的方法是通用存储库,然后当您需要上述可重用逻辑时,您可以使用其他方法创建派生存储库类型。
  • @devdigital - 对 Linq 的依赖与对数据实现的依赖不同。我想使用 Linq to objects,所以我可以排序或执行其他业务层功能。但这并不意味着我想要依赖于数据模型的实现。我在这里真正谈论的是层/层接口。例如,我希望能够更改查询,而不必在 200 个位置进行更改,如果将 IQueryable 直接推送到业务模型中就会发生这种情况。
  • @devdigital - 基本上只是将存储库的问题转移到您的业务层。你只是在改变问题。

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


【解决方案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 方法在Query 中声明)。因此无法在GroupBy 之后调用Where

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

请注意,可以多次调用Where。这会将带有AND 的新条件添加到现有条件中。这使得从单个条件以编程方式构造过滤器变得更加容易。 Having 也是如此。

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


Fluent 接口非常灵活,不需要您创建大量具有不同参数组合的方法重载。我的示例适用于字符串,但是该方法可以扩展到其他类型。您还可以为特殊情况或接受自定义类型的方法声明预定义的方法。您还可以添加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-mapper 的替代品,但可以在您使用简单字符串连接创建命令的情况下提供帮助。

【讨论】:

  • 嗯.. 有趣,但您的解决方案似乎存在 SQL 注入可能性的问题,并且并没有真正为预编译执行创建准备好的语句(因此执行速度更慢)。它可能适用于解决这些问题,但是我们被非类型安全数据集结果所困,什么不是。我更喜欢基于 ORM 的解决方案,也许我应该明确指定。这实际上是在复制 Linq 的功能,而没有从 Linq 获得的所有好处。
  • 我知道这些问题。这只是一个快速而肮脏的解决方案,展示了如何构建流畅的界面。在现实世界的解决方案中,您可能会将现有方法“烘焙”成适合您需求的流畅界面。
【解决方案2】:

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


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

public interface IQuery<TResult>
{
}

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

IQuery&lt;TResult&gt; 指定一条消息,该消息使用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&lt;TResult&gt; 接口在我们的代码中指定或注入 IQueryHandlers 时为我们提供了编译时支持。当我们将FindUsersBySearchTextQuery 改为返回UserInfo[](通过实现IQuery&lt;UserInfo[]&gt;)时,UserController 将无法编译,因为IQueryHandler&lt;TQuery, TResult&gt; 上的泛型类型约束将无法将FindUsersBySearchTextQuery 映射到User[].

然而,将IQueryHandler 接口注入消费者,还有一些不太明显的问题需要解决。我们的消费者的依赖数量可能会变得太大,并且可能导致构造函数过度注入 - 当构造函数接受太多参数时。类执行的查询数量可能会经常变化,这需要不断更改构造函数参数的数量。

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

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

IQueryProcessor 是一个具有一个泛型方法的非泛型接口。正如您在接口定义中看到的,IQueryProcessor 依赖于IQuery&lt;TResult&gt; 接口。这使我们能够在依赖于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&lt;User[]&gt; 接口,我们可以将它传递给通用的Execute&lt;TResult&gt;(IQuery&lt;TResult&gt; query) 方法。由于 C# 类型推断,编译器能够确定泛型类型,这使我们不必显式声明类型。 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&lt;TQuery, TResult&gt; 类型。此类型用于要求提供的容器类获取该类型的实例。不幸的是,我们需要使用反射调用Handle 方法(在这种情况下使用C# 4.0 dymamic 关键字),因为此时无法强制转换处理程序实例,因为通用TQuery 参数在编译时不可用时间。但是,除非 Handle 方法被重命名或获取其他参数,否则此调用将永远不会失败,如果您愿意,为此类编写单元测试非常容易。使用反射会略有下降,但没什么好担心的。


回答您的一个问题:

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

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

【讨论】:

  • 看来你得奖了。我确实喜欢这些概念,我只是希望有人能提出真正不同的东西。恭喜。
  • @FuriCuri,一个类真的需要5个查询吗?也许您可以将其视为具有太多职责的类。或者,如果正在聚合查询,那么它们实际上应该是单个查询。当然,这些只是建议。
  • @stakx 你说得对,在我最初的例子中,IQuery 接口的通用 TResult 参数没有用处。但是,在我更新的响应中,IQueryProcessorProcess 方法使用TResult 参数在运行时解析IQueryHandler
  • 我也有一个实现非常相似的博客,这让我觉得我走在正确的道路上,这是链接jupaol.blogspot.mx/2012/11/…,我已经在 PROD 应用程序中使用了一段时间,但我对这种方法有疑问。 链接和重用查询假设我有几个小查询需要组合以创建更复杂的查询,我最终只是复制了代码,但我正在寻找以获得更好,更清洁的方法。有什么想法吗?
  • @Cemre 我最终将我的查询封装在返回IQueryable 的扩展方法中,并确保不枚举集合,然后从QueryHandler 我刚刚调用/链接查询。这让我可以灵活地对我的查询进行单元测试并将它们链接起来。我的QueryHandler 之上有一个应用服务,我的控制器负责直接与该服务而不是处理程序进行对话
【解决方案3】:

我已经完成了这个,支持这个并撤消了这个。

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

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

问问自己:怎么可能?

【讨论】:

  • 嗯,我确实经历过将 Linq 集成到您的业务层的问题。它非常强大,但是当我们更改数据模型时,它就是一场噩梦。存储库改进了事情,因为我可以在本地化的地方进行更改而不会对业务层产生太大影响(除非您还必须更改业务层以支持更改)。但是,存储库变成了这些臃肿的层,大量违反了 SRP。我理解你的意思,但它也没有真正解决任何问题。
  • 如果您的数据层使用 LINQ,并且数据模型更改需要更改您的业务层...您没有正确分层。
  • 我以为你说你不再添加那个层了。当您说添加的抽象不会为您带来任何好处时,这意味着您同意 Ayende 将 nHibernate 会话(或 EF 上下文)直接传递到您的业务层。
【解决方案4】:

我的处理方式实际上是简单化且与 ORM 无关。我对存储库的看法是:存储库的工作是为应用程序提供上下文所需的模型,因此应用程序只是向存储库询问它想要的 什么,但没有告诉它如何得到它。

我为存储库方法提供了一个 Criteria(是的,DDD 样式),repo 将使用它来创建查询(或任何需要的 - 它可能是一个 web 服务请求)。恕我直言,联接和组是关于如何的细节,而不是什么和标准应该只是构建 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)。存储库实现应该知道如何将 Criteria 与底层存储或 DAO 一起使用。

我不知道您的域和模型要求,但如果最好的方法是应用程序自己构建查询,那就太奇怪了。模型变化如此之大,以至于您无法定义稳定的东西?

这个解决方案显然需要一些额外的代码,但它不会将其余的代码耦合到 ORM 或您用来访问存储的任何东西。存储库的作用是充当门面,IMO 它很干净,“标准翻译”代码是可重用的

【讨论】:

  • 这并没有解决存储库增长的问题,也没有解决不断扩大的返回各种数据的方法列表。我知道你可能没有看到这个问题(很多人没有),但其他人的看法不同(我建议阅读我链接到的文章,有很多其他人有类似的观点)。
  • 我确实解决了这个问题,因为标准使许多方法变得不必要。当然,不是所有的,如果不知道你需要什么,我就不能说太多。尽管您想直接查询数据库,但我对此印象深刻,因此,存储库可能就在路上。如果您需要直接使用关系存储,请直接使用,无需存储库。作为说明,令人讨厌的是有多少人在该帖子中引用 Ayende。我不同意,我认为许多开发人员只是以错误的方式使用该模式。
  • 它可能会在一定程度上减少问题,但如果应用程序足够大,它仍然会创建怪物存储库。我不同意 Ayende 在主逻辑中直接使用 nHibernate 的解决方案,但我同意他关于失控存储库增长的荒谬性。我不想直接查询数据库,但我也不想将问题从存储库转移到查询对象的爆炸式增长。
猜你喜欢
  • 1970-01-01
  • 2012-02-25
  • 2013-01-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-14
相关资源
最近更新 更多