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

dax.net發表於2014-08-20

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

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

相關文章