您的位置:首页 > 编程语言 > Go语言

一个完整的Mybatis分页解决方案

2014-08-12 08:53 507 查看
【布景】号码大全项目结构是 SpringMVC+Mybatis, 需求是想选用自定义的分页标签,关键词挖掘工具一起,要尽量少的影响事务程序开发的。假如你已经运用了JS结构( 如:Ext,EasyUi等)自带的分页机能,这篇文章帮助能够不大,因为JS结构供给了固定的接口。 【关于疑问】大多数分页器会运用在查询页面,要思考以下疑问:1)分页时是要随时带有近来一次查询条件2)不能影响现有的sql,相似aop的作用3)mybatis供给了通用的阻拦接口,要挑选恰当的阻拦办法和时点4)尽量少的影响现有service等接口 【关于依赖库】Google Guava 作为根底工具包Commons JXPath 用于目标查询 (1/23日版改进后,不再需求)Jackson 向前台传送Json格局数据变换用 【关于适用数据库】 如今只适用mysql (假如需求用在其他数据库可参阅 paginator的Dialect有些,改动都不大) 首先是Page类,对比简略,保留分页有关的一切信息,触及到分页算法。尽管“其貌不扬”,但很重要。后边会看到这个page类目标会以“信使”的身份出如今悉数与分页有关的当地。Java代码 保藏代码/** * 封装分页数据 */ import java.util.List; import java.util.Map; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.谷歌.common.base.Joiner; import com.谷歌.common.collect.Lists; import com.谷歌.common.collect.Maps; public class Page { private static final Logger logger = LoggerFactory.getLogger(Page.class); private static ObjectMapper mapper = new ObjectMapper(); public static String DEFAULT_PAGESIZE = "10"; private int pageNo; //当时页码 private int pageSize; //每页行数 private int totalRecord; //总记载数 private int totalPage; //总页数 private Map params; //查询条件 private Map paramLists; //数组查询条件 private String searchUrl; //Url地址 private String pageNoDisp; //能够显现的页号(分隔符"|",总页数改变时更新) private Page() { pageNo = 1; pageSize = Integer.valueOf(DEFAULT_PAGESIZE); totalRecord = 0; totalPage = 0; params = Maps.newHashMap(); paramLists = Maps.newHashMap(); searchUrl = ""; pageNoDisp = ""; } public static Page newBuilder(int pageNo, int pageSize, String url){ Page page = new Page(); page.setPageNo(pageNo); page.setPageSize(pageSize); page.setSearchUrl(url); return page; } /** * 查询条件转JSON */ public String getParaJson(){ Map map = Maps.newHashMap(); for (String key : params.keySet()){ if ( params.get(key) != null ){ map.put(key, params.get(key)); } } String json=""; try { json = mapper.writeValueAsString(map); } catch (Exception e) { logger.error("变换JSON失利", params, e); } return json; } /** * 数组查询条件转JSON */ public String getParaListJson(){ Map map = Maps.newHashMap(); for (String key : paramLists.keySet()){ Listlists = paramLists.get(key); if ( lists != null && lists.size()>0 ){ map.put(key, lists); } } String json=""; try { json = mapper.writeValueAsString(map); } catch (Exception e) { logger.error("变换JSON失利", params, e); } return json; } /** * 总件数改变时,更新总页数并核算显现款式 */ private void refreshPage(){ //总页数核算 totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : (totalRecord/pageSize + 1); //防止超出最末页(阅览途中数据被删去的状况) if ( pageNo > totalPage && totalPage!=0){ pageNo = totalPage; } pageNoDisp = computeDisplayStyleAndPage(); } /** * 核算页号显现款式 * 这儿完结以下的分页款式("[]"代表当时页号),可依据项目需求调整 * [1],2,3,4,5,6,7,8..12,13 * 1,2..5,6,[7],8,9..12,13 * 1,2..6,7,8,9,10,11,12,[13] */ private String computeDisplayStyleAndPage(){ ListpageDisplays = Lists.newArrayList(); if ( totalPage <= 11 ){ for (int i=1; i<=totalPage; i++){ pageDisplays.add(i); } }else if ( pageNo < 7 ){ for (int i=1; i<=8; i++){ pageDisplays.add(i); } pageDisplays.add(0);// 0 表明 省掉有些(下同) pageDisplays.add(totalPage-1); pageDisplays.add(totalPage); }else if ( pageNo> totalPage-6 ){ pageDisplays.add(1); pageDisplays.add(2); pageDisplays.add(0); for (int i=totalPage-7; i<=totalPage; i++){ pageDisplays.add(i); } }else{ pageDisplays.add(1); pageDisplays.add(2); pageDisplays.add(0); for (int i=pageNo-2; i<=pageNo+2; i++){ pageDisplays.add(i); } pageDisplays.add(0); pageDisplays.add(totalPage-1); pageDisplays.add(totalPage); } return Joiner.on("|").join(pageDisplays.toArray()); } public int getPageNo() { return pageNo; } public void setPageNo(int pageNo) { this.pageNo = pageNo; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public int getTotalRecord() { return totalRecord; } public void setTotalRecord(int totalRecord) { this.totalRecord = totalRecord; refreshPage(); } public int getTotalPage() { return totalPage; } public void setTotalPage(int totalPage) { this.totalPage = totalPage; } public Map getParams() { return params; } public void setParams(Map params) { this.params = params; } public Map getParamLists() { return paramLists; } public void setParamLists(Map paramLists) { this.paramLists = paramLists; } public String getSearchUrl() { return searchUrl; } public void setSearchUrl(String searchUrl) { this.searchUrl = searchUrl; } public String getPageNoDisp() { return pageNoDisp; } public void setPageNoDisp(String pageNoDisp) { this.pageNoDisp = pageNoDisp; } } 然后是最中心的阻拦器了。触及到了mybatis的中心功用,期间阅览许多mybatis源码几经修正重构,辛苦自不必说。中心思维是将阻拦到的select句子,改装成select count(*)句子,履行之得到,总数据数。再依据page中的当时页号算出limit值,拼接到select句子后。为简化代码运用了Commons JXPath 包,做目标查询。Java代码 保藏代码/** * 分页用阻拦器 */ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.Properties; import org.apache.commons.jxpath.JXPathContext; import org.apache.commons.jxpath.JXPathNotFoundException; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.parameter.DefaultParameterHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.MappedStatement.Builder; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; @Intercepts({@Signature(type=Executor.class,method="query",args={ MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })}) public class PageInterceptor implements Interceptor{ public Object intercept(Invocation invocation) throws Throwable { //当时环境 MappedStatement,BoundSql,及sql获得 MappedStatement mappedStatement=(MappedStatement)invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; BoundSql boundSql = mappedStatement.getBoundSql(parameter); String originalSql = boundSql.getSql().trim(); Object parameterObject = boundSql.getParameterObject(); //Page目标获取,“信使”抵达阻拦器! Page page = searchPageWithXpath(boundSql.getParameterObject(),".","page","*/page"); if(page!=null ){ //Page目标存在的场合,开端分页处置 String countSql = getCountSql(originalSql); Connection connection=mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection() ; PreparedStatement countStmt = connection.prepareStatement(countSql); BoundSql countBS = copyFromBoundSql(mappedStatement, boundSql, countSql); DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countBS); parameterHandler.setParameters(countStmt); ResultSet rs = countStmt.executeQuery(); int totpage=0; if (rs.next()) { totpage = rs.getInt(1); } rs.close(); countStmt.close(); * 依据原Sql句子获取对应的查询总记载数的Sql句子 */ private String getCountSql(String sql) { return "SELECT COUNT(*) FROM (" + sql + ") aliasForPage"; } public class BoundSqlSqlSource implements SqlSource { BoundSql boundSql; public BoundSqlSqlSource(BoundSql boundSql) { this.boundSql = boundSql; } public BoundSql getBoundSql(Object parameterObject) { return boundSql; } } public Object plugin(Object arg0) { return Plugin.wrap(arg0, this); } public void setProperties(Properties arg0) { } } 到展示层总算能够轻松些了,运用了文件标签来简化前台开发。选用暂时表单提交,CSS运用了Bootstrap。Html代码 保藏代码<%@tag pageEncoding="UTF-8"%> <%@ attribute name="page" type="cn.com.intasect.ots.common.utils.Page" required="true"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <% int current = page.getPageNo(); int begin = 1; int end = page.getTotalPage(); request.setAttribute("current", current); request.setAttribute("begin", begin); request.setAttribute("end", end); request.setAttribute("pList", page.getPageNoDisp()); %> <% if (current!=1 && end!=0){%> 首页 前页 <%}else{%> 首页 前页 <%} %> ${pNo} ${pNo} <% if (current 后页 末页 <%}else{%> 后页 末页 <%} %> 留意“信使”在这儿使出了浑身解数,7个首要的get办法悉数用上了。Java代码 保藏代码page.getPageNo() //当时页号 page.getTotalPage() //总页数 page.getPageNoDisp() //能够显现的页号 page.getParaJson() //查询条件 page.getParaListJson() //数组查询条件 page.getPageSize() //每页行数 page.getSearchUrl() //Url地址(作为action称号) 到这儿三个中心模块完结了。然后是阻拦器的注册。 【阻拦器的注册】需求在mybatis-config.xml 中参加阻拦器的装备Java代码 保藏代码 【有关代码修正】首先是后台代码的修正,Controller层因为触及到查询条件,需求修正的内容较多。1)入参需添加 pageNo,pageSize 两个参数2)依据pageNo,pageSize 及你的相对url结构page目标。(3)最重要的是将你的其他入参(查询条件)保留到page中4)Service层的办法需求带着page这个目标(终究意图是传递到sql履行的入参,让阻拦器识别出该sql需求分页,一起传递页号)5)将page目标传回Mode中修正前Java代码 保藏代码@RequestMapping(value = "/user/users") public String list( @ModelAttribute("name") String name, @ModelAttribute("levelId") String levelId, @ModelAttribute("subjectId") String subjectId, Model model) { model.addAttribute("users",userService.selectByNameLevelSubject( name, levelId, subjectId)); return USER_LIST_JSP; } 修正后Java代码 保藏代码@RequestMapping(value = "/user/users") public String list( @RequestParam(required = false, defaultValue = "1") int pageNo, @RequestParam(required = false, defaultValue = "5") int pageSize, @ModelAttribute("name") String name, @ModelAttribute("levelId") String levelId, @ModelAttribute("subjectId") String subjectId, Model model) { // 这儿是“信使”诞生之地,一出生就加载了许多重要信息! Page page = Page.newBuilder(pageNo, pageSize, "users"); page.getParams().put("name", name); //这儿再保留查询条件 page.getParams().put("levelId", levelId); page.getParams().put("subjectId", subjectId); - indexRead arguments from command-line "http://www.shoudashou.com"- indexRead arguments from command-line "http://www.4lunwen.cn"- indexRead arguments from command-line "http://www.zx1234.cn"- indexRead arguments from command-line "http://www.penbar.cn"- indexRead arguments from command-line "http://www.whathappy.cn"- indexRead arguments from command-line "http://www.lunjin.net"- indexRead arguments from command-line "http://www.ssstyle.cn"- indexRead arguments from command-line "http://www.91fish.cn"- indexRead arguments from command-line "http://www.fanselang.com" model.addAttribute("users",userService.selectByNameLevelSubject( name, levelId, subjectId, page)); model.addAttribute("page", page); //这儿将page回来前台 return USER_LIST_JSP; } 留意pageSize的缺省值决议该分页的每页数据行数 ,实践项目更通用的办法是运用装备文件指定。 Service层阻拦器能够主动识别在Map或Bean中的Page目标。假如运用Bean需求在里面添加一个page项目,Map则对比简略,以下是比如。Java代码 保藏代码@Override public ListselectByNameLevelSubject(String name, String levelId, String subjectId, Page page) { Map map = Maps.newHashMap(); levelId = DEFAULT_SELECTED.equals(levelId)?null: levelId; subjectId = DEFAULT_SELECTED.equals(subjectId)?null: subjectId; if (name != null && name.isEmpty()){ name = null; } map.put("name", name); map.put("levelId", levelId); map.put("subjectId", subjectId); map.put("page", page); //MAP的话加这一句就OK return userMapper.selectByNameLevelSubject(map); } 前台页面方面,因为运用了标签,在恰当的方位加一句就够了。Html代码 保藏代码 “信使”page在这儿进入标签,让分页按钮终究展示。 至此,无需修正一句sql,完结分页主动化。 【作用图】
【总结】 如今回过头来看下最开端提出的几个疑问:1)分页时是要随时带有近来一次查询条件 答复:在改造Controller层时,经过将提交参数设置到 Page目标的 Map params(单个根本型参数) 和 Map paramLists(数组根本型)处理。 趁便提一下,比如中没有触及参数是Bean的状况,实践运用中大概对比常见。简略的办法是将Bean变换层Map后参加到params。 2)不能影响现有的sql,相似aop的作用 答复:运用Mybatis供给了 Interceptor 接口,阻拦后面目一新去的件数并核算limit值,天然能神不知鬼不觉。 3)mybatis供给了通用的阻拦接口,要挑选恰当的阻拦办法和时点 答复:@Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) 只阻拦查询句子,其他增修正查不会影响。 4)尽量少的影响现有service等接口 答复:这个自认为本计划做的还不够好,首要是Controller层改造上,感觉代码量还对比大。假如有有识者晓得更好的计划还请多指导。 【遗留疑问】1)一个“显着”的功能疑问,是每次检索前都要去 select count(*)一次。在许多时分(数据改变不是格外敏感的场景)是不必要的。调整也不难,先Controller参数添加一个 totalRecord 总记载数 ,在稍加修正一下Page有关代码即可。2)要排序怎么办?这篇文章并未评论排序,但是办法是相似的。以上面代码为根底,能够较容易地完结一个通用的排序标签。 ===================================== 分割线 (1/8)=======================================关于Controller层需求将入参传入Page目标的疑问已经进行了改进,思路是主动从HttpServletRequest 类中获取入残,减低了分页代码的侵入性,具体参看文章 http://duanhengbin.iteye.com/blogs/2001142===================================== 分割线 (1/23)=======================================再次改进,运用ThreadLocal类封装Page目标,让Service层等无需传Page目标,减小了侵入性。阻拦器也省去了查找Page目标的动作,功能也一起改进。全体代码改动不大。===================================== 分割线 (2/21)=======================================今天对比闲,趁便聊下这个分页的终究版,当然历来只有不断改变的需求,没有完满的计划,这儿所说的终究版本来是一个优化后的“零侵入”的计划。为防止代码紊乱仍是只介绍思路。在上一个版本(1/23版)根底上有两点改动:一是添加一个装备文件,按Url 装备初始的每页行数。如下面这样(pagesize 指的是每页行数):Xml代码 保藏代码 二是添加一个过滤器,并将剩余的位于Control类中 唯一侵入性的分页有关代码移入过滤器。发现当时的 Url 在装备文件中有匹配是就结构Page目标,并参加到Response中。 运用终究版后,关于开发者需求分页时,只要在装备文件中加一行,并在前端页面上加一个分页标签即可,其他代码,SQL等都不需求任何改动,能够说简化到了极限。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息