一.前言
多條件查詢分頁以及排序 每個系統里都會有這個的代碼 做好這塊 可以大大提高開發效率 所以博主分享下自己的6個版本的 多條件查詢分頁以及排序
二.目前狀況
不論是ado.net 還是EF 在做多條件搜索時 都有這類似的代碼
這樣有幾個不好的地方
1.當增加查詢條件,需要改代碼,對應去寫相應的代碼。
2.對多表查詢以及or的支持 不是很好。而我們很常見的需求不可能是一個表的查詢
3. 這樣寫表示層直接出現 了SQL語句 或者 linq 的拉姆達表達式 這是很不好的 表示層不應該知道數據訪問技術
4.有的時候 我們的業務邏輯層接口是這樣的 IList<***> seach(string name,string age,string classname,int pageindex,int pagesize,string oderby)
這個時候 多一個查詢條件 對應的還要去修改業務邏輯層 EF由於傳遞的是表達式樹,則更是苦不堪言.
三.我們接下來應該實現的目標
1.當增加條件時 不需要修改代碼 只需要在view上 增加相應的查詢框即可
2.我們的多條件查詢 應該做到無關表示層技術(是否是MVC或webform)
3.應該支持多表查詢 以及OR的操作
4.應該支持更多的查詢 like in 不等於 等操作
5.關於分頁 不應該與數據訪問耦合在一起 個人感覺 分頁只需要知道總條數 以及當前頁數 和每頁多少條 然后生成分頁代碼即可 不應該與EF等耦合到一起 分頁應該是獨立出來 可控制的
6.客戶可以自己添加搜索條件 這是個強大的功能 想怎么查 客戶自己添加即可
7. 統一查詢接口 做到有條件增加 不修改代碼
8.分頁應該支持 url重寫或者 mvc路由 不應該生成的連接只是?pageindex=值 這種的
四.我實現的幾個多條件查詢分頁版本以適應各種需求(每篇會寫一個版本的實現以及代碼的提供,有好意見的歡迎留言)
1.url get提交版 實現URL分頁多條件查詢以及排序的好處是 我們可以把當前的搜索條件 當前頁數 排序等 都在url上顯示 可以方便的發給好友 以及后退等瀏覽器操作(個人給dudu老大建議,博客園應該做成這種的)
2.post 提交版本的 搜索條件較大 不適合用url的
3.ajax+mvc版本的 (關於AJAX實現 我認為有兩種 1.服務端實現好內容的拼接,傳輸給客戶端 2直接傳遞json給客戶端 客戶端來做拼接)
這個版本會實現服務端拼接內容 好處是 服務端做拼接簡單 能做更多的事情 維護服務端代碼方便 尤其是強大的Razor
4.ajax+webapi+Knockoutjs 版本
這個版本 我實現的是 服務端只是傳遞 json 這樣服務端效率很高 喜歡這樣開發方式的朋友 就是前端拼接字符是很不好的 代碼會顯得很亂 這個時候 前端就需要一個模版引擎我用的是 jquery-temp 配合強大的Knockoutjs
5.動態增加查詢條件版
這個版本我實現的是 客戶可以自己添加查詢條件 查詢條件是動態的
6.移植到webform版
7.EF應該得到表達式樹 讓EF自己生成SQL語句 這樣方便擴展 實現其他方法
五.url get提交版開始
廢話了那么多 今天就寫下url get版的多條件查詢 以及分頁 排序
先上個丑陋界面的截圖 界面雖丑 但是功能齊全 查詢 分頁 排序齊全
看下控制器的代碼
我們這里沒有各種條件判斷 判斷哪個為空 使用哪個排序的判斷 我們的業務邏輯層接口 沒有接受表達式樹的參數 與數據訪問層不是耦合的 而是使用了Querymodel對象 來抽象所有查詢條件
這樣 這個對象 可以翻譯成 EF的表達式樹 也可以翻譯成SQL語句 所以我們的 表示層MVC 不用非要使用EF的的底層
而我們的分頁 只需要知道當前頁數 總數據 以及 每頁大小 去自動生成分頁
關於分頁 很多人喜歡把這個擴展htmlhelper 做成 html.pager 這種做法 這樣很多表示層的展示 都會耦合到 這個里面 舉個例子,比如把分頁的布局 從 table 變成 ul 這樣的 這是純表示層的
本應該修改 view 現在卻要去改htmlhelper 而且你的分頁代碼越強大 則htmlhelper 里的內容越多 修改起來越不容易 所以我的意見是 做分頁的 最好使用html.Partial 然后把分頁的邏輯寫到
部分頁里 這樣就實現了分頁 只關注分頁 與其他的一切都沒有關系 我們要做的就是構建 Pager類 然后傳遞給模版即可 例如
這個版本要注意的就是 分頁后要保存查詢條件
六.url get提交版實現分析
1.先來說下多條件查詢吧
我們要做的是把各個條件構建成QueryModel 而如何構建則是關鍵 我們想下 獲得mvc提交的內容的是根據name 所以我們可以固定一個name的格式 如
ID(like操作): <input type="text" name="[Contains]StuId" value="@StuId" />
這樣我們可以通過正則獲取想要信息的部分 然后在模型綁定構建出QueryModel對象 然后再把這個對象 翻譯成SQL語句 或者表達式樹 就可以用於ado 或者 EF操作了
順便重點說下 這個思路 園子里的重典早都實現了 而且實現的很好 大家可以去他的博客看下 以后的各個版本的分頁 都會用這個表達式樹構建為基礎 這是他文章的鏈接 重典老哥的實現
(ps:這周剛剛見過他本人,聊得甚歡,開心.希望以后還可以多聚聚)
不過重典老哥的表達式樹構建 我改成自己的實現了 整體思路還是一樣的 自戀的說下 ~ 我的可讀性更高些 哈哈 因為重典已經描述的很詳細了 具體可以看我的代碼和他的文章
當然我們得到 通過QueryModel 得到的表達式樹 通過擴展方法 直接調用Where方法即可 讓我們看下我們的EF的業務邏輯層
public class StudentService:IStudentService { /// <summary> /// 按條件搜索 /// </summary> /// <param name="query">搜索條件</param> /// <param name="pageIndex">當前頁數(索引從1開始)</param> /// <param name="pageSize">每頁顯示條數</param> /// <param name="total">總條數</param> /// <param name="orderBy">排序字段</param> /// <param name="ascending">是否升序</param> /// <returns></returns> public IList<Student> Search(QueryModel query, int pageIndex, int pageSize, out int total, string orderBy, bool ascending) { IList<Student> stulist = Builder<Student>.CreateListOfSize(321).TheFirst(44).With(x => x.StuName = "hy").And(x => x.Nullint = 1).And(x => x.LoveGril = "LILI").And(x => x.CreateTime = new DateTime(2012, 02, 03)).And(x => x.Birthday = new DateTime(2012, 09, 01)).And(x => x.Stuclass = new StuClass() { ClassId = "2", ClassName = "二班" }).TheNext(33).With(x => x.StuName = "wlf").And(x => x.Nullint = 2).And(x => x.LoveGril = "MM").And(x => x.CreateTime = new DateTime(2012, 06, 06)).And(x => x.Birthday = new DateTime(2012, 09, 010)).And(x => x.Stuclass = new StuClass() { ClassId = "1", ClassName = "一班" }).TheNext(244).And(x=>x .Stuclass=new StuClass(){ClassId = "3", ClassName = "三班"}).Build(); var dbcontext = stulist.AsQueryable(); //模擬EF context 假設數據庫里原數據為200條 dbcontext = dbcontext.Where(query); total = dbcontext.Count();//執行查詢數量sql dbcontext = dbcontext.OrderBy(orderBy, ascending).Skip(pageSize * (pageIndex - 1)).Take(pageSize); return dbcontext.ToList();//執行分頁排序查詢sql } }
這里說下 為了大家調試方便 不用真正的數據庫 用了測試神奇NBuilder 來模擬的 然后轉換成AsQueryable 來模擬EF的
看上面代碼 可以看到 沒有了各個分支的條件判斷 以及排序的判斷 以后多出查詢條件 不需要修改業務邏輯層了~
2.接下來說下url get提交的思路
上問說過自己的觀點 分頁最好能放到部分頁 而不是擴展htmlhelper 所以我的實現方式是把構建Pager分頁視圖類 傳遞給 部分視圖 來做如何展示 一些分頁有關的邏輯封裝在 Pager里
下面是Pager的代碼
public class Pager { public Pager(int currentPageIndex, int totalItemCount, int pagesize = 20) { this.TotalItemCount = totalItemCount; this.PageSize = pagesize; this.CurrentPageIndex = currentPageIndex > TotalPageCount ? 1 : currentPageIndex; } /// <summary> /// 當前第幾頁 /// </summary> public int CurrentPageIndex { get; set; } /// <summary> /// 每頁顯示多少條 /// </summary> public int PageSize { get; set; } /// <summary> /// 總共多少條記錄 /// </summary> public int TotalItemCount { get; set; } /// <summary> /// 總共多少頁 /// </summary> public int TotalPageCount { get { double pageCount = (double)TotalItemCount / (double)PageSize; pageCount = Math.Ceiling(pageCount); return (int)pageCount; } } /// <summary> /// 是否顯示 /// </summary> public bool IsShow { get { if (TotalPageCount > 0) { return true; } else { return false; } } } /// <summary> /// 是否顯示上一頁 /// </summary> public bool HasPreviousPage { get { return (CurrentPageIndex > 1); } } /// <summary> /// 是否顯示下一頁 /// </summary> public bool HasNextPage { get { return (CurrentPageIndex < TotalPageCount); } } }
而分頁的部分視圖 只用根據這個類 去管理如何展示
這里分享幾個小技巧
技巧一.
因為我們要實現下面需求
1. 我們要保存以前的URL 信息再里面 不能讓以前的消失 2 需要支持 url路由后的 分頁 比如 控制器/方法/Page1 而不是?pageIndex=1 3.因為很多個頁面都要用到 所以要與方法名解耦
實現這個的技巧關鍵 就是使用RouteValueDictionary

var queryString = ViewContext.HttpContext.Request.QueryString; var dict = new System.Web.Routing.RouteValueDictionary(ViewContext.RouteData.Values); foreach (string item in queryString.Keys) { dict[item] = queryString[item]; }
通過上面的代碼 就可以保存住以前的參數了 接着就是
dict["PageIndex"] = 1;//設置頁碼 @Html.RouteLink("首頁", dict);
這樣就可以設置分頁 以及解耦控制器 和方法名了~
如果你設置了類似的路由
routes.MapRoute("Page", "{controller}/{action}/page{PageIndex}", new { controller = "WLFQuery", action = "Index", PageIndex = 1 });
那么也是支持的~
2.技巧二
因為我們的分頁 支持通過下拉框選擇頁碼后自動跳頁 有點兒像webform 的autopostback 而我們又是get 提交版的 需要解決一個問題 選擇后自動跳頁 需要帶上以前的查詢條件 不能跳完頁以后 查詢條件消失了
這里采用的辦法 借鑒了一下 aspnetpager 作者的 urlpager的思路
用一個隱藏的a 標簽 這個a 標簽生成的連接 保存了當前的查詢條件等 他的頁面數 顯示成"*pageindex* 當我們選擇下拉框跳頁后 用選擇的值 替換這個"*pageindex* 然后跳轉到當前href 則解決了上述問題
上代碼
<text>跳轉至</text>
<select id="pageselect" onchange="selectchange()">
@for (int i = 1; i <=@Model.TotalPageCount; i++)
{
var selected = "";
if (i==Model.CurrentPageIndex)
{
selected = "selected='selected'";
}
<option value="@i" @selected>@i</option>
}
</select>
{
dict["PageIndex"] = "*pageindex*";
}
<a style="display:none" id="pagelink" href="@Url.RouteUrl(dict)" ></a>
//
<script type="text/javascript">
function selectchange() {
var pageselect = document.getElementById("pageselect");
var pageselectValue = pageselect.options[pageselect.selectedIndex].value;
var linkdom= document.getElementById("pagelink");
var href = linkdom.href;
href = href.replace("*pageindex*", pageselectValue);
window.location = href;
}
</script>
<text>頁</text>
3.點擊搜索時 get 提交 並回到第一頁
<form action="@Url.Action("index", new { PageIndex = 1 })" method="get"> </form>
具體代碼可以下載源碼看~
七. URL GET提交的遺憾
上面基本功能已經實現 但是有些遺憾的是 大家知道 mvc沒有viewstate機制 而且我們的又是URL分頁 及時是webform 也會面臨這個問題
雖然我們通過url保存了搜索的狀態 但是沒有解決點擊分頁后 搜索框的內容還在。所以會出來一個奇怪的現象 按條件搜索完 比如搜索姓名為hy的 然后點擊分頁 搜索框的內容沒了
但是分頁后內容卻還是hy的 因為我們的url 里存的有搜索結果 但是重新加載后 文本框的內容卻沒了 這樣雖然無傷大雅 但是還是略感不爽 如果非要解決 也不是沒有辦法 只是覺得都不是很完美
方案1.
ID(like操作): <input type="text" name="[Contains]StuId" value="@Request.QueryString["[Contains]StuId"]" /><br />
因為我們的url里保存的有真實的搜索條件 所以我們可以通過Request.QueryString["[Contains]StuId"] 直接得到 但是我覺得在MVC里 出現Request.QueryString 這樣的信息 不好看
方案2.
ViewContext.Controller.ValueProvider.GetValue("[Contains]StuId").RawValue 通過這樣 也可以得到值
方案3.
在控制器里
ViewBag.Query = querymodel; 記錄搜索條件 然后在view里讀出來
QueryModel query=ViewBag.Query as QueryModel; string StuId=query.Items.Where(x => x.Field == "StuId").Select(x=>x.Value).FirstOrDefault()!=null?query.Items.Where(x => x.Field == "StuId").Select(x=>x.Value).FirstOrDefault().ToString():"";
上面三個方式 感覺都有不完美的地方 因為我們還原的內容 不只是 text 有可能是 多選框 下拉列表 等 所以要做處理 就稍微麻煩了下 比如
string[] newarrlovewgril = (string[])arrlovegril; MMischeck= newarrlovewgril.Contains("MM") ? "checked='checked'" : ""; LUCIischeck = newarrlovewgril.Contains("LILI") ? "checked='checked'" : ""; GAGAischeck = newarrlovewgril.Contains("GAGA") ? "checked='checked'" : ""; MM<input type="checkbox" name="lovegril" value="MM" @MMischeck /> LILI<input type="checkbox" name="lovegril" value="LILI" @LUCIischeck /> GAGA<input type="checkbox" name="lovegril" value="GAGA" @GAGAischeck />
我再想 把這些還原也做成自動化的 不用任何代碼的 求集思廣益 希望大家給些建議
八.總結
EF實現動態查詢以及排序的關鍵 就在於表達式樹的構建。所以學會並理解表達式樹很關鍵,這個不盡在這里有用,用來代替反射也可以提高效率~不過表達式樹的生成 建議實際項目中 加上緩存。
表達式樹的學習 可以看下這幾篇
http://www.cnblogs.com/Terrylee/archive/2008/08/01/custom-linq-provider-part-1-expression-tree.html
http://www.cnblogs.com/Ninputer/archive/2009/08/28/expression_tree1.html
http://msdn.microsoft.com/en-us/library/bb397951.aspx
短短文章 花了半天才寫完~ 希望對大家有幫助 或者能給大家一些啟示 有問題可以留言交流 歡迎批評和建議 感謝閱讀~
順便幫忙宣傳下 我在的兩個QQ群吧 33353329 205217091 有興趣的可以進來討論
過幾天介紹另兩個版本 POST提交版和 MVC+EF AJAX 多條件查詢 分頁 排序
最后附上本節代碼下載 (代碼比較粗糙,主要做演示)
轉載請說明來自 http://www.cnblogs.com/wlflovenet/archive/2012/11/30/MVC_EntityFramework_Query.html