使用 LINQKit PredicateBuilder 解決動態OR條件查詢窘境
問題簡述
在使用傳統SQL語法方式來進行DB資料撈取時,當面對搜尋條件是動態且不固定時,我們往往會使用SQL語法拼裝的方式來解決;當進入Entity Framework世界後就必須與LINQ搏鬥,以下將舉例說明此篇文章所面臨之問題,以及要如何利用LINQKit PredicateBuilder來排除。
以商品搜尋功能為例,使用者可透過商品名稱與生產工廠(AND)來搜尋指定商品
搜尋功能邏輯大致如下,可直接使用Where方法串接來達到AND累加條件作用
public ActionResult Index(string productName, string manufactory)
{
// 搜尋條件為 = 產品名稱 AND 生產工廠
IQueryable<Product> products = db.Products;
// 如果有輸入產品名稱作為搜尋條件時
if (!string.IsNullOrWhiteSpace(productName))
{products = products.Where(p => p.Name.Contains(productName)); }
// 如果有輸入生產工廠作為搜尋條件時
if (!string.IsNullOrWhiteSpace(manufactory))
{ products = products.Where(p => p.Manufactory.Contains(manufactory)); }
// 回傳搜尋結果
return View(products.Include(p => p.ProductType));
}
此時需求加入,希望能以商品類別作為搜尋條件(OR),畫面如下
由於產品類型條件將是以OR作為搜尋條件的累加,而LINQ中WHERE串連是以AND作為條件結合,明顯的不符合我們的需求(瓶頸出現),此時就是考驗工程師解決問題(Google)能力啦! 所幸小弟遇到的問題其實前輩們早已突破,這盞明燈就是LINQKit中提供的PredicateBuilder類別工具,以下將對此進行問題排除實作。
解決方案
LINQKit提供了許多LINQ to SQL / EF的擴充功能(Extensions),可以讓使用者在面對LINQ先天缺憾的時候有一道曙光出現。在此我們僅使用到PredicateBuilder來解決問題,有興趣的朋友可以進一步探索其他功能。
首先,了解用法最快方式就是將原本的Code調整套用PredicateBuilder作為搜尋條件,請先加入LinqKit命名空間。其中PredicateBuilder.True<Product>()表示搜尋初始條件為True,通常用於AND累加條件上(若是用於OR累加條件上,無論其他條件為何皆成立);反之若是使用OR累加條件時,則會使用PredicateBuilder.False<Product>()作為初始條件。此用意在於無任何搜尋條件時仍可回傳正確資料。
再來就是處理本篇重點OR條件累加。做法與AND條件差不多,不同的部分就是給予False為初始條件,因此在未勾選任何商品類別情況下,由於初始值是False故不回傳任何資料。最後在透過原有Where串接特性來結合搜尋條件1(predicate)及搜尋條件2(predicateProductType),依照輸入值來撈取正確資料,做法如下。
public ActionResult Index(string productName, string manufactory, int[] productTypes)
{
IQueryable<Product> products = db.Products;
// #1# 搜尋條件為 = True AND 產品名稱 AND 生產工廠
var predicate = PredicateBuilder.True<Product>();
// 如果有輸入產品名稱作為搜尋條件時
if (!string.IsNullOrWhiteSpace(productName))
{ predicate = predicate.And(p => p.Name.Contains(productName)); }
// 如果有輸入生產工廠作為搜尋條件時
if (!string.IsNullOrWhiteSpace(manufactory))
{ predicate = predicate.And(p => p.Manufactory.Contains(manufactory)); }
// #2# 搜尋條件為 = False OR 產品類型1 OR 產品類型2 ...
var predicateProductType = PredicateBuilder.False<Product>();
// 如果有輸入產品類型作為搜尋條件時
if (productTypes!=null)
{
foreach (var type in productTypes)
{ predicateProductType = predicateProductType.Or(p => p.ProductTypeId == type); }
}
// #3# 搜尋條件為 = True AND 產品名稱 AND 生產工廠 AND (False OR 產品類型1 OR 產品類型2 ...)
// 回傳搜尋結果
return View(products.AsExpandable().Where(predicate)
.Where(predicateProductType).Include(p => p.ProductType));
}
最後進行驗證,預設搜尋條件下取得所有資料
[測試一] 正確撈出名稱包含外套的所有商品
[測試二] 正確撈出中國製的服飾或家電商品
LINQPad中使用PredicateBuilder方法
相信許多人都會使用LINQPad這套工具來測試自己的LINQ語法,當然我們也可以在LINQPad中使用PredicateBuilder所帶來的方便性,話不多說兩個步驟就解決了,請參考以下說明。
首先,按下F4鍵叫出Query Properties設定畫面,勾選Include PredicateBuilder選項
加入LinqKit.dll 參考 (請自行至LINQKit官網下載)
最後切換頁面至Additional Namespace Imports後,新增命名空間LinqKit並設為預設值即可
如此即可在LINQPad中使用LINQKit PredicateBuilder
參考資要
http://www.albahari.com/nutshell/linqkit.aspx
http://www.albahari.com/nutshell/predicatebuilder.aspx
http://kelp.phate.org/2011/12/linq-to-object-linqkitpredicatebuilder.html
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !