一种通用查询语言的定义与实践

最近发现在项目中或许会遇到让用户自己构建查询表达式的情况。比如需要通过一种可配置的界面,来让用户输入一组具有逻辑关系的查询表达式,然后根据这个查询表达式来过滤并返回所需要的数据。这种用户案例其实非常常见。由此受到启发,或许我们可以自己定义一种通用的面向查询的领域特定语言(DSL),来实现查询的序列化和动态构建。

概述

由此我发布了一个称为Unified Queries(以下简称UQ)的开源项目,UQ定义了一种DSL,用以描述一种查询的特定结构。它同时还提供了将查询规约(Query Specification)转换为SQL WHERE子句以及Lambda表达式的功能。UQ提供了非常灵活的框架设计,能够非常方便地通过实现IQuerySpecificationCompiler接口,或者继承QuerySpecificationCompiler<T>抽象类来自定义查询规约的转换功能。

DSL结构定义

下面的XSD架构(XSD Schema)定义了UQ的DSL语义,需要注意的是,它包含了一组递归的层次结构:

例子

假定在QuerySpecificationSample.xml文件中定义了如下的查询规约,在执行该查询规约时,系统将返回所有名字以“Peter”开头,并且姓氏中不含有“r”字符,以及年收入在30000以上的客户。

<?xml version="1.0" encoding="utf-8"?>
<QuerySpecification>
  <LogicalOperation Operator="And">
    <Expression Name="FirstName" Type="String" Operator="StartsWith" Value="Peter"/>
    <UnaryLogicalOperation Operator="Not">
      <LogicalOperation Operator="Or">
        <Expression Name="LastName" Type="String" Operator="Contains" Value="r"/>
        <Expression Name="YearlyIncome" Type="Decimal" Operator="LessThanOrEqualTo" Value="30000"/>
      </LogicalOperation>
    </UnaryLogicalOperation>
  </LogicalOperation>
</QuerySpecification>

以下C#代码将根据该xml文件产生SQL的WHERE子句:

static void Main(string[] args)
{
    var querySpecification = QuerySpecification.LoadFromFile("QuerySpecificationSample.xml");
    var compiler = new SqlWhereClauseCompiler();
    Console.WriteLine(compiler.Compile(querySpecification));
}

所产生的SQL WHERE子句如下:

((FirstName LIKE 'Peter%') AND (NOT ((LastName LIKE '%r%') OR (YearlyIncome <= 30000))))

然而在很多情况下,ADO.NET的开发人员更喜欢通过使用DbParameter来指定查询中所包含的参数值,而不是简单地将参数拼接在SQL语句中。UQ通样能够产生带有参数列表的SQL WHERE子句。要达到这样的效果,仅需在初始化SqlWhereClauseCompiler时,将构造函数参数设置为true即可:

var compiler = new SqlWhereClauseCompiler(true);

于是产生的SQL WHERE子句就是:

((FirstName LIKE @fvP8gN) AND (NOT ((LastName LIKE @ESzoyd) OR (YearlyIncome <= @fG5Z7e))))

参数值则可以通过SqlWhereClauseCompiler的ParameterValues属性获得。

事实上SqlWhereClauseCompiler所产生的SQL WHERE子句是满足Microsoft SQL Server需要的,如果您希望能够产生符合Oracle或MySQL语法的WHERE子句,可以自己扩展SqlWhereClauseCompiler类来实现。

接下来,下面的C#代码可以将上面的xml文件中所定义的查询规约编译成Lambda表达式:

static void Main(string[] args)
{
    var querySpecification = QuerySpecification.LoadFromFile("QuerySpecificationSample.xml");
    var compiler = new LambdaExpressionCompiler<Customer>();
    Console.WriteLine(compiler.Compile(querySpecification));
}

产生的Lambda表达式如下:

p => (p.FirstName.StartsWith("Peter") AndAlso Not((p.LastName.Contains("r") OrElse (p.YearlyIncome <= 30000))))

下面的C#例子详细描述了如何在一组客户对象上应用查询规约,并将满足条件的客户数据返回:

private static Customer[] GetAllCustomers()
{
    return new[]
               {
                   new Customer { FirstName = "Sunny", LastName = "Chen", YearlyIncome = 10000 },
                   new Customer { FirstName = "PeterJam", LastName = "Yo", YearlyIncome = 10000 },
                   new Customer { FirstName = "PeterR", LastName = "Ko", YearlyIncome = 50000 },
                   new Customer { FirstName = "FPeter", LastName = "Law", YearlyIncome = 70000 },
                   new Customer { FirstName = "Jim", LastName = "Peter", YearlyIncome = 30000 }
               };
}

static void Main(string[] args)
{
    var querySpecification = QuerySpecification.LoadFromFile("QuerySpecificationSample.xml");
    var compiler = new LambdaExpressionCompiler<Customer>();
    var customers = GetAllCustomers();
    foreach (var customer in customers.Where(compiler.Compile(querySpecification).Compile()))
    {
        Console.WriteLine(
            "FirstName: {0}, LastName: {1}, YearlyIncome: {2}",
            customer.FirstName,
            customer.LastName,
            customer.YearlyIncome);
    }
}

总结

现在我们已经有了一种查询结构的DSL定义,这就使得一个查询规约可以保存在内存的对象中,也可以被持久化到外部的存储系统,比如xml文件中,或者数据库中。接下来我们可以设计一种通用的界面,通过这个界面来设计一个查询规约,于是,就可以通过Compiler将所设计的查询规约转换为另一种可被已有系统接受的形式。更进一步,我们还可以设计一系列的Builder,将SQL WHERE子句或者Lambda表达式转换为UQ中的查询规约。

希望这个小项目能够给大家带来启发和帮助。

posted @ 2014-08-20 20:13  dax.net  阅读(5922)  评论(13编辑  收藏  举报