-->

精心设计的查询命令和/或技术规格(Well designed query commands and/

2019-07-18 11:09发布

我一直在寻找了一段时间,很好地解决了由典型库模式出现的问题(日益增长的专门的查询方法等列表..看到: http://ayende.com/blog/3955/repository-是最新单 )。

我真的很喜欢使用命令查询,特别是通过使用该规范模式的想法。 然而,我与规范的问题是,它仅涉及到简单的选择的标准(基本上,where子句),并且不与查询的其他问题,如加入,分组,子集选择或投影等处理..基本上,所有的额外箍许多查询必须经过以获得正确的一组数据。

(注:我用的是“指挥”一词作为Command模式,也被称为查询对象我说的不是命令,在命令/查询分离那里是查询和命令之间进行区分(更新,删除,插入))

所以我在寻找封装了整个查询的替代品,但仍足够灵活,你不只是换面条库的命令类的爆炸。

我已经使用,比如Linqspecs,而我在能够有意义的名称指定选择标准找到一些价值,它只是不够的。 也许我正在寻找,结合多种方法的混合解决方案。

我找其他人可能已经发展到无论是解决这个问题,还是解决不同的问题,但仍然满足这些需求的解决方案。 在链接的文章,Ayende建议直接使用NHibernate的情况下,但我觉得你的业务层主要是复杂化,因为它现在还必须包含查询信息。

我会在这个只要等待期间可经过提供赏金。 所以,请让你的解决方案,赏金值得,具有很好的解释和我将选择最佳的解决方案,最多给予好评的选手。

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

更新:我真的很惊讶,没有很多好的建议在这里。 好像人要么完全CQRS,或者他们在仓库营完全是。 我的大多数应用程序是不是很复杂,值得CQRS(东西大多数CQRS主张爽快地说,你不应该使用它)。

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

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

Answer 1:

免责声明:由于没有任何伟大的解答,我决定发布从一个伟大的博客帖子我刚才读的一部分,复制几乎一字不差。 你可以找到完整的博客文章在这里 。 所以在这里,它是:


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

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>将不能够映射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方法被重命名或获取其他的参数,这个调用永远不会失败,如果你想,这是很容易为此类编写单元测试。 使用反射会给略有下降,但没有什么真正担心。


要回答你的问题之一:

所以我在寻找封装了整个查询的替代品,但仍足够灵活,你不只是换面条库的命令类的爆炸。

采用这种设计的结果是会有很多小班制度,但有很多小/集中班(有明确的名称)是一件好事。 这种方法就是具有与在信息库中同一方法不同的参数,大量重载显然要好的多,因为您可以将那些在一个查询类。 所以,你仍然得到少了很多查询类,比在存储库的方法。



Answer 2:

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

我公司供应的仓库方法与标准(是的,DDD风格),这将通过回购可以用来创建查询(或任何需要 - 这可能是一个Web服务请求)。 加入和团体恕我直言,是如何,而不是什么和标准应该是唯一的基地建立一个where子句的详细信息。

模型=由应用neede最终对象或数据结构。

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。

我不知道你的域和模型的要求,但它会很奇怪,如果最好的办法是应用程序构建查询本身。 该模型的变化这么多,你无法定义的东西稳定吗?

该解决方案显然需要一些额外的代码,但它的不夫妇其余的ORM或任何你正在使用访问存储。 存储库,它的工作作为一个门面和国际海事组织它的清洁和“标准转换”的代码是可重复使用



Answer 3:

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

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

有趣的部分是,你回答了你自己的问题响应奥利弗的回答是:“这基本上是重复的LINQ的功能,而你的LINQ得到的所有好处”。

问问自己:怎么会没有呢?



Answer 4:

您可以使用流利的接口。 其基本思想是,一个类的方法返回当前实例这个非常具有类执行一些动作之后。 这允许您链方法调用。

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

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方法(其中方法在声明Query )。 因此,它不可能叫WhereGroupBy

然而并非十全十美。 有了这个类层次结构则可连续隐藏成员,但没有表现出新的问题。 因此, Having抛出时,它之前被调用异常GroupBy

请注意,这是可以调用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映射器的替代品,但可以在你创建一个使用简单的字符串连接,否则命令的情况下帮助。



文章来源: Well designed query commands and/or specifications