一種通用查詢語言的定義與實踐


最近發現在項目中或許會遇到讓用戶自己構建查詢表達式的情況。比如需要通過一種可配置的界面,來讓用戶輸入一組具有邏輯關系的查詢表達式,然后根據這個查詢表達式來過濾並返回所需要的數據。這種用戶案例其實非常常見。由此受到啟發,或許我們可以自己定義一種通用的面向查詢的領域特定語言(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中的查詢規約。

希望這個小項目能夠給大家帶來啟發和幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM