一种通用查询语言的定义与实践
最近发现在项目中或许会遇到让用户自己构建查询表达式的情况。比如需要通过一种可配置的界面,来让用户输入一组具有逻辑关系的查询表达式,然后根据这个查询表达式来过滤并返回所需要的数据。这种用户案例其实非常常见。由此受到启发,或许我们可以自己定义一种通用的面向查询的领域特定语言(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中的查询规约。
希望这个小项目能够给大家带来启发和帮助。