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