程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> Linq to Sql:N層應用中的查詢(下): 根據條件進行動態查詢

Linq to Sql:N層應用中的查詢(下): 根據條件進行動態查詢

編輯:關於.NET

如果允許在UI層直接訪問Linq to Sql的DataContext,可以省去很多問題,譬如在處理多表join的時 候,我們使用var來定義L2S查詢,讓編譯器自動推斷變量的具體類型 (IQueryable<匿名類型>), 並提供友好的智能提示;而且可以充分應用L2S的延遲加載特性,來進行動態查詢。但如果我們希望將業 務邏輯放在一個獨立的層中(譬如封裝在遠程的WCF應用中),又希望在邏輯層應用Linq to sql,則情況 就比較復雜了;由於我們只能使用var(IQueryable<匿名類型>),而var只能定義方法(Method)范 圍中聲明的變量,出了方法(Method)之後IDE就不認得它了;在這種對IQueryable<匿名類型>一無 所知的情況下,又希望能在開發時也能應用上IDE的智能感應,我們該怎麼定義層之間交互的數據傳輸載 體呢?又如何對它進行動態查詢呢?

內容比較多,分上下兩篇,上篇了介紹查詢返回自定義實體,本篇介紹動態查詢。

在我們的日常開發中,時常需要根據用戶在UI上的輸入來進行動態查詢。在Ado.Net主宰的舊石器時 代,一般會這樣來動態拼接SQL查詢條件:

1: string filter = " 1=1";
2: if(XXOO文本框不為空)
3:     filter  += string.Format(" AND XXOO='{0}', XXOO)";
4: if(OOXX文本框不為空)
5:      filter += string.Format(" AND OOXX='{0}', OOXX)";
6: gridView.DataSource =  BusinessLogic.XOXOQuery(filter);

然後將過濾條件傳給業務邏輯層,由業務邏輯層拼接出完整的TSQL語句。 但到了LINQ to SQL時代 ,我們該辦了呢?還要繼續玩字符串拼接游戲嗎?

後面將以NorthWind為例,動態查詢產品(Product)及其供應商信息(Supplier):

1: partial  class ProductExt : Products
   2: {
   3:     public string  CompanyName { get; set; }
   4: }

1. UI層直接訪問 DataContext

如果使用L2S查詢延遲加載的特性,動態查詢也變得相當簡單:

1:  public void TestDynamicQuery()
   2: {
   3:     using  (NorthWindDataContext context = new NorthWindDataContext())
   4:     {
   5:         var query = from P in context.Products
   6:                             join S in context.Suppliers
    7:                             on P.SupplierID equals  S.SupplierID
   8:                            select  new
   9:                            {
  10:                                 P.ProductName,
  11:                                 P.UnitPrice,
  12:                                 P.QuantityPerUnit,
  13:                                 S.CompanyName
  14:                             };
  15:         if (XXOO)
  16:             query = query.Where(p =>  p.ProductName.Contains("che"));
  17:         if(OOXX)
  18:              query = query.Where(p => p.UnitPrice >= 20);
  19:          gridView.DataSource = query.ToList(); //延遲加載,ToList時才進行運算
  20:         gridView.DataBind();
  21:     }
  22: }

看起來還是比較舒服的,不用再繼續拼接SQL了,開發時也可以充分利用IDE的智能感應。

但也不是無可挑剔,這裡的邏輯無法復用。假如另外一個應用場景,要根據供應商名稱來查詢產品信 息,我們該怎麼處理呢,另外再寫一個查詢?如果再多一個引用場景呢,難道我們每次都要Ctrl+C | Ctrl +V?還是把這個邏輯封裝在業務邏輯層,讓多個的頁面都可以使用?

2. 分層後引發的問題

分層的好處之一就是邏輯復用。在Ado.Net時代,我們可以把這個join操作放在業務邏輯層,UI層只 需要根據不同的應用場景,拼接where條件,然後傳給業務邏輯層處理即可。

當在分層應用中使用L2S時,如果想把這個邏輯放到業務邏輯層,我們或許可以這樣做:

2.1.  繼續拼接

或許我們想過繼續按照舊石器時代的做法,直接拼接;但是我們立刻會發現顯然是行不通的,我們無 法“直接”將L2S查詢與字符串進行拼接。

2.2. 構造Expression或者Func

query.Where()可以接受一個表達式Expression<Func<TSource, bool> predicate>或者 委托Func<TSource, bool> predicate,或許我們想過嘗試構造這樣的Expression或者Func;但是 我們又會遇到新的問題,如上面的查詢,我們的query的類型是 IQueryable<匿名類型>,匿名類 型的定義是在編譯階段才由編譯器創建的,開發時我們根本不知道TSource是類型,又該怎麼創建這樣的 Expression或者Func呢?

3. 使用Dynamic LINQ繼續拼接游戲

上面2.1中提到無法“直接”將L2S查詢與字符串進行拼接,但是可以通過一些擴展來間接達到目的, 網上已經有人這麼做了,具體可以參考:Dynamic LINQ。下面是一個示例:

1: Northwind db = new Northwind(connString);
2: var query =
3:      db.Customers.Where("City == @0 and Orders.Count >= @1", "London", 10).
4:      OrderBy("CompanyName").
5:     Select("New(CompanyName as Name,  Phone)");

看起來,貌似我們又可以繼續玩字符串拼接了。不過需要注意的一點兒是,這裡拼接的字符串不再是 TSQL中的字符串命令了,而是L2S查詢。這是基於如下原因:在L2S中,查詢被表示為一個表達式目錄樹 (Expression Tree,表示的是數據,不是代碼),待需要訪問查詢結果集時(針對延遲加載的情況),這棵 樹才被對應的Provider(這裡用的是SQL Server,所以對應的是SqlProvider)翻譯為TSQL,並發送給 ADO.Net來執行;Dynamic LINQ就是將傳進來的字符串解析為表達式目錄樹,並與原來的L2S進行適當地 合並,從而得到最終的表達式目錄樹。

根據字符串進行拼接,是一種解決辦法。但是這樣做有個不好的地方,就是我們失去了IDE的智能感 應。

4. 對IQueryable進行動態查詢擴展

上面2.2節中,還提到了另外一種處理思路,那就是構造Expression或者Func;當然,這裡會遇到上 面提到的問題:我們的query的類型是IQueryable<匿名類型>,開發時根本不知道其具體類型,如 何創建Expression<Func<匿名類型, bool> predicate>或者委托Func<匿名類型, bool> predicate呢?

下面是我實現過程中的那艱苦卓越的辛酸歷程:

還是拿上面的查詢作為例子,譬如要查詢ProductName.Contains("che")) && UnitPrice >= 20的記錄;則我們能構造出來的及需要構造出來的表達式會是什麼樣子呢?下面是兩者之間的差 距:

Expression<Func<Products, bool>> predicate = t =>  t.ProductName.Contains("che") && t.UnitPrice >= 22 //Can  Do
Expression<Func<匿名類型, bool>> predicate = t =>  t.ProductName.Contains("che") && t.UnitPrice >= 22 //To DO

差距呢?乍一看,這就是一對雙胞胎啊,還需要轉換個啥子,吃飽撐的啊……

不過細看之後,二者確有不同之處,下面是補全後的對比:

Expression<Func<Products, bool>> predicate = (Products  t)  => t.ProductName.Contains("che") && t.UnitPrice >=  22
Expression<Func<匿名類型, bool>> predicate = (匿名類型 t) =>  t.ProductName.Contains("che") && t.UnitPrice >= 22

現在可以看到,這不是一對普通的雙胞胎,基因中的軟色體都不是一個樣子,這是一對龍鳳胎。由於 .Net是強類型語言,IEnumerable<TSource>.Where()方法只認得後者,而拒絕接受前者,因此接 下來,我們的目標是……沒有蛀牙?NO,基因手術……當然,也希望手術的副作用包括沒有蛀牙(bug)。

上一篇中,我實現了一個對象轉換器,可以把一個對象轉換成另一個對象;但這裡用不上,這裡需要 換的是基因,需要把一種類型換成另一種類型。所以需要急切實現的一個函數就是,能把一個 LambdaExpression的參數類型換成另一種類型,於是我實現了下面的方法:(其中,TSource為源類型, TResult為目標類型)

public static Expression<Func<TResult, bool>> Replace<TSource,  TResult>(
this Expression<Func<TSource, bool>> predicate)
{
ParameterConverter pc = new ParameterConverter(predicate);
return  (Expression<Func<TResult, bool>>)pc.Replace(typeof(TResult));
}

在開始寫這段代碼之前,我的表達式目錄樹知識幾乎為0;於是又開始翻MSDN,找到了這裡:LINQ 中 的表達式目錄樹……最終在MSDN的幫助下,我終於把它給實現出來了,完成後我不禁沾沾自喜(雖然只有 幾十行代碼,可行代碼不到十來行,剩下的是從MSDN中的ExpressionVisitor盜版的,但也耗了我整整一 個半天)……

古人雲:樂極生悲。看來這句話還是有道理的。慶幸之後,接著我又墜入了萬丈深淵,因為我不知道 怎麼調用這個方法!在這個方法外部,我們的query是IQueryable<匿名類型>,在IQueryable< 匿名類型>.Where()方法中,嘗試調用這個Replace方法的時候,我不知道該傳什麼類型參數給 TResult。我又白干了……

前面提到:在L2S中,查詢被表示為一個表達式目錄樹(Expression Tree,表示的是數據,不是代碼) 。我既然可以向上面這樣修改Expression<Func<TSource, bool>>,那我應該也可以修改這 個這個LINQ查詢,而且Dynamic LINQ也正是在修改LINQ查詢啊。

看了下Dynamic LINQ中對Where的擴展,才知道IQueryable公布了其Provider屬性: IQueryable.Provider,我們可可以直接調用Provider.CreateQuery來對原有的query進行擴充:

Expression<Func<Products, bool>> predicate = t =>  t.ProductName.Contains("che") && t.UnitPrice >= 22;
IQueryable query =  from P in context.Products
join S in context.Suppliers
on P.SupplierID  equals S.SupplierID
select new
{
P.ProductName,
P.UnitPrice,
P.QuantityPerUnit,
S.CompanyName
};
query  = query.Provider.CreateQuery(
Expression.Call(
typeof(Queryable), "Where",
new  Type[] { query.ElementType }, 
query.Expression, predicate.Replace<Products> (query.ElementType)));

前面,我無法完成將TSource(Products)強制轉換成匿名類型;但這裡,通過構造Expression,來將 類型弱化,最終將通過Expression的編譯和執行功能,來實現這種轉換。

當然,每次這樣寫的話,我也覺得麻煩;於是,就有了下面的對IQueryable的擴展:

public static IQueryable DynamicWhere<T>(this IQueryable query,  Expression<Func<T, bool>> predicate)
{
if (predicate == null)
return query;

return query.Provider.CreateQuery(
Expression.Call (
typeof(Queryable), "Where",
new Type[] { query.ElementType  },
query.Expression, predicate.Replace<T>(query.ElementType)));
}

//然後就可以這樣用了:

public List<ProductExt> TestDynamicQuery3()
{
using  (NorthWindDataContext context = new NorthWindDataContext())
{
IQueryable query  = from P in context.Products
join S in context.Suppliers
on P.SupplierID  equals S.SupplierID
select new
{
P.ProductName,
P.UnitPrice,
P.QuantityPerUnit,
S.CompanyName,
S.Address
};
query = query.DynamicWhere((Products p) => p.ProductName.Contains("che"))
.DynamicWhere((Suppliers s) => s.Address == "P.O. Box 78934") 
//.DynamicWhere((ProductExt p) => p.CompanyName == p.ProductName)  //BinaryExpression右邊不能有對參數的引用
.DynamicWhere((Products p) =>  p.UnitPrice >= 5 * 4 + 2);

return query.ConvertTo<ProductExt> ();
}
}

由於Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)已經被可恥地占去了,所以這裡我定義了 一個自己的方法名:DynamicWhere。

最後,來說說這種方法的不足之處:

(1). 由於我們在Expression<Func<TSource, bool>> predicate時,使用的源類型 TSource與query中元素類型(匿名類型)之間的屬性集可能存在不同,因此這裡的Expression中,只能使 用匿名類型中已經聲明的屬性,使用不屬於該匿名類型的屬性時,編譯時不會抱錯,但運行時會出錯。 例如,我還補充傳入了一個根據供應商所在城市的過濾條件:.DynamicWhere((Suppliers s) => s.City== "London"),運行時就掛了……這就又遇到上一節中同樣的問題:UI層怎麼知道屬性可用,哪 些屬性被閹割了呢?這又是一個問題,暫時只能說:源代碼前沒有秘密。

(2). BinaryExpression中的右側表達式不能包括對參數的應用。譬如上面代碼中注釋掉的一行,引 用了參數p,執行會報錯;這是因為在處理類型參數轉換時,我對BinaryExpressio中的右側表達式和 CallExpression中的參數表達式進行了運算,轉得到常量表達式。應該還有更好的思路,判斷這些 Expresion是否引用了參數,如果引用了參數,則不進行運算,如果沒有引用參數,則進行運算。但是我 還沒有考慮出來該怎麼來判斷……於是就成了這個樣子。不過對於動態查詢來說,一般情況下應該夠用 了,以後想到更好的思路再加進去。

5. 如何進行邏輯復用

為了將思路描述清楚,前面我只介紹了如何進行動態查詢,而刻意避開了一個問題,就是如何進行邏 輯復用。問題要分解開來,然後再逐個擊破~

5.1 UI與業務邏輯層位於同一地址空間(同一個應用程序域)

既然位於同一地址空間,那就可以在UI層創建Expression<Func<TSource, bool>> predicate,然後傳入業務邏輯層:

public List<ProductExt> TestDynamicQuery(Expression<Func<ProductExt,  bool>> predicate)
{
using (NorthWindDataContext context = new  NorthWindDataContext())
{
IQueryable query = from P in context.Products
join  S in context.Suppliers
on P.SupplierID equals S.SupplierID
select new
{
P.ProductName,
P.UnitPrice,
P.QuantityPerUnit,
S.CompanyName
};
return  query.DynamicWhere(predicate).ConvertTo<ProductExt>();
}
}
//不同場景下的 應用:
//場景1
Expression<Func<ProductExt, bool>> predicate = t  => t.ProductName.Contains("che") && t.UnitPrice >= 22;
return  TestDynamicQuery2(predicate);
//場景2
Expression<Func<ProductExt, bool>>  predicate = t => t.CompanyName == "New Orleans Cajun Delights";
return  TestDynamicQuery2(predicate);

但是這樣又引入了新的問題,如何根據用戶的輸入條件,動態構造這個呢? t => t.ProductName.Contains("che") && t.UnitPrice >= 22;

雖然可以像下面5.2一樣來處理,但是也還是有點兒麻煩;理想情況下,我希望可以像下面這樣來構 造predicate,這樣,我們就可以使用&、| 、&=、|=來任意拼接過濾條件了:

1: Expression<Func<ProductExt, bool>> predicate = null;
2:  predicate &= (t => t.ProductName.Contains("che")) | (t => t.UnitPrice  >= 22);

5.2 UI與業務邏輯層位於不同地址空間(跨應用程序域)

如果UI與業務邏輯位於不同的地址空間,Expression<Func<TSource, bool>> predicate就沒有辦法跨進程傳遞。

一個可選辦法是,將各個查詢條件值作為參數(如果參數較多的話,或者經常變化的話,可以引入參 數對象,具體可參考《重構》),傳到業務邏輯然後再構造Expression。如果您有好的思路,歡迎一起交 流。

1: NorthWindDataContext context = new NorthWindDataContext();
2:
3:  public List<ProductExt> TestDynamicQuery(string productName, decimal?  unitPrice)
4: {
5:     IQueryable query = TestDynamicQueryAll(); //開放了 IQueryable,延遲加載
6:     if (!string.IsNullOrEmpty(productName))
7:          query = query.DynamicWhere((ProductExt p) => p.ProductName.Contains (productName));
8:     if (unitPrice.HasValue)
9:         query =  query.DynamicWhere<ProductExt>(p => p.UnitPrice >=  unitPrice);
10:
11:     return query.ConvertTo<ProductExt>();
12: }
13:
14: protected IQueryable TestDynamicQueryAll()
15: {
16:      IQueryable query = from P in context.Products
17:                         join S in context.Suppliers
18:                          on P.SupplierID equals S.SupplierID
19:                         select new   //匿名類型
20:                         {
21:                            P.ProductName,
22:                             P.UnitPrice,
23:                             P.QuantityPerUnit,
24:                             S.CompanyName
25:                         };
26:     return query;  //延遲加載
27: }

如果允許IQueryable滿天飛的話,就沒有5.1中提到的動態構造Expression的麻煩問題了。但是貌似 看起來還是有點兒煩,能不能了個繼續偷懶呢?

出處:http://happyhippy.cnblogs.com/

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved