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


90

我一直在寻找很多时间来找到典型存储库模式(不断增长的专用查询方法列表等)所提出的问题的好的解决方案。请参阅:http : //ayende.com/blog/3955/repository-是新单人)。

我真的很喜欢使用Command查询的想法,特别是通过使用Specification模式。但是,我的规范问题是它仅涉及简单选择的标准(基本上是where子句),而没有处理其他查询问题,例如联接,分组,子集选择或投影等。基本上,许多查询必须经过所有额外的步骤才能获得正确的数据集。

(请注意:我在“命令”模式中使用“命令”一词,也称为查询对象。我并不是在谈论命令/查询分离,而是在查询和命令之间进行区分(更新,删除,插))

因此,我正在寻找替代方案来封装整个查询,但仍具有足够的灵活性,以至于您不只是将意粉存储库换成大量的命令类。

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

我正在寻找别人可能已开发出的解决方案,以解决此问题或解决其他问题,但仍满足这些要求。在链接的文章中,Ayende建议直接使用nHibernate上下文,但我认为这会使您的业务层大大复杂化,因为它现在还必须包含查询信息。

等待期过后,我将为此提供赏金。因此,请为您的解决方案提供值得的奖励,并提供良好的解释,我将选择最佳的解决方案,并支持亚军。

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

更新:我真的很惊讶这里没有很多好的建议。似乎人们要么完全是CQRS,要么完全处于存储库阵营中。我的大多数应用程序都不够复杂,不足以保证CQRS(大多数CQRS倡导者很容易地说,您不应将其用于此)。

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

理想情况下,我正在寻找的是查询对象,规范模式和存储库之间的某种交叉。就像我在上面说的那样,规范模式仅处理where子句方面,而不处理查询的其他方面,例如联接,子选择等。存储库处理整个查询,但过一会儿就会失去控制。查询对象还处理整个查询,但是我不想简单地用大量的查询对象代替存储库。


5
很棒的问题。我也想看看比我建议更多的人。目前,我正在编写代码库,其中通用存储库还包含Command对象或Query对象的重载,其结构类似于Ayende在其博客中描述的结构。PS:这也可能引起程序员的注意。
西蒙·怀特海德2013年

如果您不介意对LINQ的依赖,为什么不只使用公开IQueryable的存储库呢?一种通用方法是通用存储库,然后当您需要上述可重用逻辑时,可以使用其他方法创建派生存储库类型。
devdigital

@devdigital-对Linq的依赖与对数据实现的依赖不同。我想对对象使用Linq,以便可以对其他业务层功能进行排序或执行。但这并不意味着我想要依赖数据模型的实现。我在这里真正谈论的是层/层接口。例如,我希望能够更改查询,而不必在200个地方进行更改,如果直接将IQueryable推入业务模型,就会发生这种情况。
Erik Funkenbusch

1
@devdigital-基本上只是将存储库中的问题移到您的业务层中。您只是在解决问题。
Erik Funkenbusch

Answers:


94

免责声明:由于还没有很好的答案,我决定发表一篇我不久前读的很棒的博客文章的一部分,几乎逐字抄写。您可以在此处找到完整的博客文章。所以这里是:


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

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代码时,该接口为我们提供了编译时支持。当我们改为(通过实现)将FindUsersBySearchTextQueryreturn 更改为return时,将无法编译,因为的通用类型约束将无法映射至。UserInfo[]IQuery<UserInfo[]>UserControllerIQueryHandler<TQuery, TResult>FindUsersBySearchTextQueryUser[]

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现在依赖于IQueryProcessor能够处理所有的查询。所述UserControllerSearchUsers方法调用IQueryProcessor.Process传递初始化的查询对象的方法。由于FindUsersBySearchTextQuery实现了IQuery<User[]>接口,因此我们可以将其传递给泛型Execute<TResult>(IQuery<TResult> 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<TQuery, TResult>基础上,提供的查询实例的类型类型。此类型用于要求提供的容器类获取该类型的实例。不幸的是,我们需要Handle使用反射调用该方法(在这种情况下,使用C#4.0 dymamic关键字),因为在这一点上,由于通用TQuery参数在编译时不可用,因此无法转换处理程序实例。但是,除非Handle重命名该方法或获取其他参数,否则此调用将永远不会失败,并且如果您愿意的话,为此类编写单元测试非常容易。使用反射会稍有下降,但不必担心。


要回答您的问题之一:

因此,我正在寻找替代方案来封装整个查询,但仍具有足够的灵活性,以至于您不只是将意粉存储库换成大量的命令类。

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


2
看起来您获得了大奖。我喜欢这些概念,我只是希望有人提出一些与众不同的东西。恭喜。
Erik Funkenbusch 2013年

1
@FuriCuri,一个类真的需要5个查询吗?也许您可以将其视为职责过多的班级。或者,如果正在汇总查询,则也许它们实际上应该是单个查询。当然,这些只是建议。
2013年

1
@stakx绝对正确,在我最初的示例TResult中,IQuery接口的通用参数没有用。但是,在我更新的响应中,TResult参数由Process方法使用IQueryProcessorIQueryHandler在运行时解析。
david.s 2013年

1
我也有一个博客,该博客的实现非常相似,这使我觉得自己处在正确的位置,这是链接jupaol.blogspot.mx/2012/11/…,并且我在PROD应用程序中使用了一段时间,但我对这种方法有疑问。链接和重用查询假设我有几个小查询需要合并以创建更复杂的查询,最后我只是复制了代码,但我正在寻找一种更好,更干净的方法。有任何想法吗?
Jupaol 2013年

4
@Cemre我最终将查询封装在返回的Extension方法中IQueryable,并确保不枚举集合,然后从QueryHandler我刚刚调用/链接查询。这使我可以灵活地对查询进行单元测试并进行链接。我上方有一个应征服务QueryHandler,我的控制器负责直接与该服务而不是处理程序进行对话
Jupaol 2015年

4

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

我为存储库方法提供了一个条件(是,DDD样式),该标准将由存储库用于创建查询(或任何需要的条件-它可能是一个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是干净的,并且“标准翻译”代码可重复使用


这没有解决存储库增长的问题,也没有解决不断增长的返回各种数据的方法的列表。我知道您可能不会对此有问题(很多人没有),但是其他人对此有不同的看法(我建议阅读与我链接的文章,很多其他人对此有相似的看法)。
Erik Funkenbusch 2013年

1
我确实解决了这个问题,因为该标准使许多方法变得不必要。当然,在其中的全部中,我对我所需要的内容一无所知。尽管您想直接查询数据库,但给我留下了深刻的印象,因此,存储库可能就这样。如果您需要解决关系问题,请直接使用它,而无需存储库。值得注意的是,有多少人在该帖子中引用了Ayende。我不同意这一观点,并且我认为许多开发人员只是错误地使用了该模式。
MikeSW 2013年

1
它可以在某种程度上减少问题,但是如果有足够大的应用程序,它仍然会创建怪物存储库。我不同意Ayende在主要逻辑中直接使用nHibernate的解决方案,但是我确实同意他关于存储库增长失控的荒谬说法。我不想直接查询数据库,但我也不想将问题从存储库转移到大量查询对象。
Erik Funkenbusch 2013年

2

我已经做到了,支持了这一点,却撤消了。

主要的问题是:无论如何执行,添加的抽象都无法获得独立性。根据定义它将泄漏。本质上,您只是在发明一个整个层,只是为了让您的代码看起来更可爱……但这并不会减少维护,提高可读性或使您获得任何类型的模型不可知论。

有趣的部分是,您根据Olivier的回答回答了自己的问题:“这实质上是在不从Linq获得所有好处的情况下复制Linq的功能”。

问问自己:怎么可能?


好吧,我肯定遇到了将Linq集成到您的业务层中的问题。它非常强大,但是当我们更改数据模型时,这是一场噩梦。使用存储库可以改善情况,因为我可以在本地化的位置进行更改,而又不会对业务层产生太大影响(除非您还必须更改业务层以支持更改)。但是,存储库变成了这些肿的层,从而严重违反了SRP。我理解您的意思,但也不能真正解决任何问题。
Erik Funkenbusch 2013年

如果您的数据层使用LINQ,并且数据模型更改需要更改您的业务层...您的分层不正确。
Stu 2013年

我以为您是在说您不再添加该层。当您说添加的抽象不能给您带来任何好处时,表明您与Ayende达成了将nHibernate会话(或EF上下文)直接传递到业务层的协议。
Erik Funkenbusch

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方法返回a GroupedQuery,它是的基类,Query并且没有Where方法(在中声明where方法Query)。因此,无法在Where之后致电GroupBy

但是,它并不完美。使用该类层次结构,您可以连续隐藏成员,但不显示新成员。因此Having,在调用before之前引发异常GroupBy

请注意,可以Where多次呼叫。这会AND在现有条件的基础上增加新条件。这使得从单个条件以编程方式构造过滤器变得更加容易。使用也是可能的Having

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


流利的接口非常灵活,不需要您使用不同的参数组合来创建大量方法重载。我的示例使用字符串,但是该方法可以扩展到其他类型。您还可以为特殊情况或接受自定义类型的方法声明预定义的方法。您也可以添加类似ExecuteReader或的方法ExceuteScalar<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映射器的替代,但是在您可能会使用简单字符串串联创建命令的情况下会有所帮助。


3
嗯..有趣,但是您的解决方案似乎在SQL注入可能性方面存在问题,并且并未真正为预编译执行创建准备好的语句(因此执行速度较慢)。它可能适用于解决这些问题,但后来我们陷入了非类型安全数据集结果以及其他问题。我希望使用基于ORM的解决方案,也许我应该明确指定它。这实质上是在不从Linq获得所有好处的情况下复制Linq的功能。
Erik Funkenbusch

我知道这些问题。这只是一个快速而肮脏的解决方案,显示了如何构建流畅的界面。在实际解决方案中,您可能会将现有方法“烘焙”到适合您需求的流畅界面中。
Olivier Jacot-Descombes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.