一.前言
多条件查询分页以及排序 每个系统里都会有这个的代码 做好这块 可以大大提高开发效率 所以博主分享下自己的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
通过上面的代码 就可以保存住以前的参数了 接着就是
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
短短文章 花了半天才写完~ 希望对大家有帮助 或者能给大家一些启示 有问题可以留言交流 欢迎批评和建议 感谢阅读~