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

2023-10-31

原文地址:http://duanhengbin.iteye.com/blog/1998017

参考地址:http://blog.csdn.net/isea533/article/details/23831273

Mybatis 的物理分页是应用中的一个难点,特别是配合检索和排序功能叠加时更是如此。

我在最近的项目中开发了这个通用分页器,过程中参考了站内不少好文章,新年第一天,特此发文回馈网站。

特别鸣谢 paginator项目 (https://github.com/miemiedev/mybatis-paginator ) ,阅读源码帮助很大。

 

【背景】

项目框架是 SpringMVC+Mybatis, 需求是想采用自定义的分页标签,同时,要尽量少的影响业务程序开发的。
如果你已经使用了JS框架( 如:Ext,EasyUi等)自带的分页机能,是属于前端分页,不在本文讨论范围。

 

【关于问题】

大多数分页器会使用在查询页面,要考虑以下问题:

1)分页时是要随时带有最近一次查询条件

2)不能影响现有的sql,类似aop的效果

3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点

4)尽量少的影响现有service等接口

 

【关于依赖库】

Google Guava    作为基础工具包

Commons JXPath  用于对象查询  (1/23日版改善后,不再需要)

Jackson  向前台传送Json格式数据转换用

 

【关于适用数据库】 

现在只适用mysql 

(如果需要用在其他数据库可参考 paginator的Dialect部分,改动都不大)

 

首先是Page类,比较简单,保存分页相关的所有信息,涉及到分页算法。虽然“其貌不扬”,但很重要。后面会看到这个page类对象会以“信使”的身份出现在全部与分页相关的地方。

Java代码   收藏代码
  1. /** 
  2.  * 封装分页数据 
  3.  */  
  4. import java.util.List;  
  5. import java.util.Map;  
  6.   
  7. import org.codehaus.jackson.map.ObjectMapper;  
  8. import org.slf4j.Logger;  
  9. import org.slf4j.LoggerFactory;  
  10.   
  11. import com.google.common.base.Joiner;  
  12. import com.google.common.collect.Lists;  
  13. import com.google.common.collect.Maps;  
  14.   
  15. public class Page {  
  16.   
  17.   private static final Logger logger = LoggerFactory.getLogger(Page.class);  
  18.   private static ObjectMapper mapper = new ObjectMapper();  
  19.   
  20.   public static String DEFAULT_PAGESIZE = "10";  
  21.   private int pageNo;          //当前页码  
  22.   private int pageSize;        //每页行数  
  23.   private int totalRecord;      //总记录数  
  24.   private int totalPage;        //总页数  
  25.   private Map<String, String> params;  //查询条件  
  26.   private Map<String, List<String>> paramLists;  //数组查询条件  
  27.   private String searchUrl;      //Url地址  
  28.   private String pageNoDisp;       //可以显示的页号(分隔符"|",总页数变更时更新)  
  29.   
  30.   private Page() {  
  31.     pageNo = 1;  
  32.     pageSize = Integer.valueOf(DEFAULT_PAGESIZE);  
  33.     totalRecord = 0;  
  34.     totalPage = 0;  
  35.     params = Maps.newHashMap();  
  36.     paramLists = Maps.newHashMap();  
  37.     searchUrl = "";  
  38.     pageNoDisp = "";  
  39.   }  
  40.      
  41.   public static Page newBuilder(int pageNo, int pageSize, String url){  
  42.     Page page = new Page();  
  43.     page.setPageNo(pageNo);  
  44.     page.setPageSize(pageSize);  
  45.     page.setSearchUrl(url);  
  46.     return page;  
  47.   }  
  48.     
  49.   /** 
  50.    * 查询条件转JSON 
  51.    */  
  52.   public String getParaJson(){  
  53.     Map<String, Object> map = Maps.newHashMap();  
  54.     for (String key : params.keySet()){  
  55.       if ( params.get(key) != null  ){  
  56.         map.put(key, params.get(key));  
  57.       }  
  58.     }  
  59.     String json="";  
  60.     try {  
  61.       json = mapper.writeValueAsString(map);  
  62.     } catch (Exception e) {  
  63.       logger.error("转换JSON失败", params, e);  
  64.     }  
  65.     return json;  
  66.   }  
  67.   
  68.   /** 
  69.    * 数组查询条件转JSON 
  70.    */  
  71.   public String getParaListJson(){  
  72.     Map<String, Object> map = Maps.newHashMap();  
  73.     for (String key : paramLists.keySet()){  
  74.       List<String> lists = paramLists.get(key);  
  75.       if ( lists != null && lists.size()>0 ){  
  76.         map.put(key, lists);  
  77.       }  
  78.     }  
  79.     String json="";  
  80.     try {  
  81.       json = mapper.writeValueAsString(map);  
  82.     } catch (Exception e) {  
  83.       logger.error("转换JSON失败", params, e);  
  84.     }  
  85.     return json;  
  86.   }  
  87.   
  88.   /** 
  89.    * 总件数变化时,更新总页数并计算显示样式 
  90.    */  
  91.   private void refreshPage(){  
  92.     //总页数计算  
  93.     totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : (totalRecord/pageSize + 1);  
  94.     //防止超出最末页(浏览途中数据被删除的情况)  
  95.     if ( pageNo > totalPage && totalPage!=0){  
  96.         pageNo = totalPage;  
  97.     }  
  98.     pageNoDisp = computeDisplayStyleAndPage();  
  99.   }  
  100.     
  101.   /** 
  102.    * 计算页号显示样式 
  103.    *  这里实现以下的分页样式("[]"代表当前页号),可根据项目需求调整 
  104.    *   [1],2,3,4,5,6,7,8..12,13 
  105.    *   1,2..5,6,[7],8,9..12,13 
  106.    *   1,2..6,7,8,9,10,11,12,[13] 
  107.    */  
  108.   private String computeDisplayStyleAndPage(){  
  109.     List<Integer> pageDisplays = Lists.newArrayList();  
  110.     if ( totalPage <= 11 ){  
  111.       for (int i=1; i<=totalPage; i++){  
  112.         pageDisplays.add(i);  
  113.       }  
  114.     }else if ( pageNo < 7 ){  
  115.       for (int i=1; i<=8; i++){  
  116.         pageDisplays.add(i);  
  117.       }  
  118.       pageDisplays.add(0);// 0 表示 省略部分(下同)  
  119.       pageDisplays.add(totalPage-1);         
  120.       pageDisplays.add(totalPage);  
  121.     }else if ( pageNo> totalPage-6 ){  
  122.       pageDisplays.add(1);  
  123.       pageDisplays.add(2);  
  124.       pageDisplays.add(0);  
  125.       for (int i=totalPage-7; i<=totalPage; i++){  
  126.         pageDisplays.add(i);  
  127.       }         
  128.     }else{  
  129.       pageDisplays.add(1);  
  130.       pageDisplays.add(2);  
  131.       pageDisplays.add(0);  
  132.       for (int i=pageNo-2; i<=pageNo+2; i++){  
  133.         pageDisplays.add(i);  
  134.       }  
  135.       pageDisplays.add(0);  
  136.       pageDisplays.add(totalPage-1);  
  137.       pageDisplays.add(totalPage);  
  138.     }  
  139.     return Joiner.on("|").join(pageDisplays.toArray());  
  140.   }  
  141.    
  142.   public int getPageNo() {  
  143.      return pageNo;  
  144.   }  
  145.    
  146.   public void setPageNo(int pageNo) {  
  147.      this.pageNo = pageNo;  
  148.   }  
  149.    
  150.   public int getPageSize() {  
  151.      return pageSize;  
  152.   }  
  153.    
  154.   public void setPageSize(int pageSize) {  
  155.      this.pageSize = pageSize;  
  156.   }  
  157.    
  158.   public int getTotalRecord() {  
  159.      return totalRecord;  
  160.   }  
  161.    
  162.   public void setTotalRecord(int totalRecord) {  
  163.     this.totalRecord = totalRecord;  
  164.     refreshPage();       
  165.   }  
  166.   
  167.   public int getTotalPage() {  
  168.      return totalPage;  
  169.   }  
  170.    
  171.   public void setTotalPage(int totalPage) {  
  172.      this.totalPage = totalPage;  
  173.   }  
  174.    
  175.   public Map<String, String> getParams() {  
  176.      return params;  
  177.   }  
  178.      
  179.   public void setParams(Map<String, String> params) {  
  180.      this.params = params;  
  181.   }  
  182.     
  183.   public Map<String, List<String>> getParamLists() {  
  184.     return paramLists;  
  185.   }  
  186.   
  187.   public void setParamLists(Map<String, List<String>> paramLists) {  
  188.     this.paramLists = paramLists;  
  189.   }  
  190.   public String getSearchUrl() {  
  191.     return searchUrl;  
  192.   }  
  193.   public void setSearchUrl(String searchUrl) {  
  194.     this.searchUrl = searchUrl;  
  195.   }  
  196.   public String getPageNoDisp() {  
  197.     return pageNoDisp;  
  198.   }  
  199.   public void setPageNoDisp(String pageNoDisp) {  
  200.     this.pageNoDisp = pageNoDisp;  
  201.   }  
  202. }  

 

然后是最核心的拦截器了。涉及到了mybatis的核心功能,期间阅读大量mybatis源码几经修改重构,辛苦自不必说。

核心思想是将拦截到的select语句,改装成select count(*)语句,执行之得到,总数据数。再根据page中的当前页号算出limit值,拼接到select语句后。

为简化代码使用了Commons JXPath 包,做对象查询。

Java代码   收藏代码
  1. /** 
  2.  * 分页用拦截器 
  3.  */  
  4. import java.sql.Connection;  
  5. import java.sql.PreparedStatement;  
  6. import java.sql.ResultSet;  
  7. import java.util.Properties;  
  8.   
  9. import org.apache.commons.jxpath.JXPathContext;  
  10. import org.apache.commons.jxpath.JXPathNotFoundException;  
  11. import org.apache.ibatis.executor.Executor;  
  12. import org.apache.ibatis.executor.parameter.DefaultParameterHandler;  
  13. import org.apache.ibatis.mapping.BoundSql;  
  14. import org.apache.ibatis.mapping.MappedStatement;  
  15. import org.apache.ibatis.mapping.MappedStatement.Builder;  
  16. import org.apache.ibatis.mapping.ParameterMapping;  
  17. import org.apache.ibatis.mapping.SqlSource;  
  18. import org.apache.ibatis.plugin.Interceptor;  
  19. import org.apache.ibatis.plugin.Intercepts;  
  20. import org.apache.ibatis.plugin.Invocation;  
  21. import org.apache.ibatis.plugin.Plugin;  
  22. import org.apache.ibatis.plugin.Signature;  
  23. import org.apache.ibatis.session.ResultHandler;  
  24. import org.apache.ibatis.session.RowBounds;  
  25.   
  26. @Intercepts({@Signature(type=Executor.class,method="query",args={ MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })})  
  27. public class PageInterceptor implements Interceptor{  
  28.   
  29.   public Object intercept(Invocation invocation) throws Throwable {  
  30.       
  31.     //当前环境 MappedStatement,BoundSql,及sql取得  
  32.     MappedStatement mappedStatement=(MappedStatement)invocation.getArgs()[0];      
  33.     Object parameter = invocation.getArgs()[1];   
  34.     BoundSql boundSql = mappedStatement.getBoundSql(parameter);   
  35.     String originalSql = boundSql.getSql().trim();  
  36.     Object parameterObject = boundSql.getParameterObject();  
  37.   
  38.     //Page对象获取,“信使”到达拦截器!  
  39.     Page page = searchPageWithXpath(boundSql.getParameterObject(),".","page","*/page");  
  40.   
  41.     if(page!=null ){  
  42.       //Page对象存在的场合,开始分页处理  
  43.       String countSql = getCountSql(originalSql);  
  44.       Connection connection=mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection()  ;            
  45.       PreparedStatement countStmt = connection.prepareStatement(countSql);    
  46.       BoundSql countBS = copyFromBoundSql(mappedStatement, boundSql, countSql);  
  47.       DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countBS);  
  48.       parameterHandler.setParameters(countStmt);  
  49.       ResultSet rs = countStmt.executeQuery();  
  50.       int totpage=0;  
  51.       if (rs.next()) {    
  52.         totpage = rs.getInt(1);    
  53.       }  
  54.       rs.close();    
  55.       countStmt.close();    
  56.       connection.close();  
  57.         
  58.       //分页计算  
  59.       page.setTotalRecord(totpage);  
  60.         
  61.       //对原始Sql追加limit  
  62.       int offset = (page.getPageNo() - 1) * page.getPageSize();  
  63.       StringBuffer sb = new StringBuffer();  
  64.       sb.append(originalSql).append(" limit ").append(offset).append(",").append(page.getPageSize());  
  65.       BoundSql newBoundSql = copyFromBoundSql(mappedStatement, boundSql, sb.toString());  
  66.       MappedStatement newMs = copyFromMappedStatement(mappedStatement,new BoundSqlSqlSource(newBoundSql));    
  67.       invocation.getArgs()[0]= newMs;    
  68.     }  
  69.     return invocation.proceed();  
  70.       
  71.   }  
  72.     
  73.   /** 
  74.    * 根据给定的xpath查询Page对象 
  75.    */  
  76.   private Page searchPageWithXpath(Object o,String... xpaths) {  
  77.     JXPathContext context = JXPathContext.newContext(o);  
  78.     Object result;  
  79.     for(String xpath : xpaths){  
  80.       try {  
  81.         result = context.selectSingleNode(xpath);  
  82.       } catch (JXPathNotFoundException e) {  
  83.         continue;  
  84.       }  
  85.       if ( result instanceof Page ){  
  86.         return (Page)result;  
  87.       }  
  88.     }  
  89.     return null;  
  90.   }  
  91.   
  92.   /** 
  93.    * 复制MappedStatement对象 
  94.    */  
  95.   private MappedStatement copyFromMappedStatement(MappedStatement ms,SqlSource newSqlSource) {  
  96.     Builder builder = new Builder(ms.getConfiguration(),ms.getId(),newSqlSource,ms.getSqlCommandType());  
  97.       
  98.     builder.resource(ms.getResource());  
  99.     builder.fetchSize(ms.getFetchSize());  
  100.     builder.statementType(ms.getStatementType());  
  101.     builder.keyGenerator(ms.getKeyGenerator());  
  102.     builder.keyProperty(ms.getKeyProperty());  
  103.     builder.timeout(ms.getTimeout());  
  104.     builder.parameterMap(ms.getParameterMap());  
  105.     builder.resultMaps(ms.getResultMaps());  
  106.     builder.resultSetType(ms.getResultSetType());  
  107.     builder.cache(ms.getCache());  
  108.     builder.flushCacheRequired(ms.isFlushCacheRequired());  
  109.     builder.useCache(ms.isUseCache());  
  110.       
  111.     return builder.build();  
  112.   }  
  113.   
  114.   /** 
  115.    * 复制BoundSql对象 
  116.    */  
  117.   private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql, String sql) {  
  118.     BoundSql newBoundSql = new BoundSql(ms.getConfiguration(),sql, boundSql.getParameterMappings(), boundSql.getParameterObject());  
  119.     for (ParameterMapping mapping : boundSql.getParameterMappings()) {  
  120.         String prop = mapping.getProperty();  
  121.         if (boundSql.hasAdditionalParameter(prop)) {  
  122.             newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));  
  123.         }  
  124.     }  
  125.     return newBoundSql;  
  126.   }  
  127.   
  128.   /** 
  129.    * 根据原Sql语句获取对应的查询总记录数的Sql语句 
  130.    */  
  131.   private String getCountSql(String sql) {  
  132.     return "SELECT COUNT(*) FROM (" + sql + ") aliasForPage";  
  133.   }  
  134.   
  135.   public class BoundSqlSqlSource implements SqlSource {    
  136.       BoundSql boundSql;    
  137.       public BoundSqlSqlSource(BoundSql boundSql) {    
  138.         this.boundSql = boundSql;    
  139.       }    
  140.       public BoundSql getBoundSql(Object parameterObject) {    
  141.         return boundSql;    
  142.       }    
  143.     }    
  144.   public Object plugin(Object arg0) {  
  145.      return Plugin.wrap(arg0, this);  
  146.   }  
  147.   public void setProperties(Properties arg0) {  
  148.   }  
  149. }  

 

到展示层终于可以轻松些了,使用了文件标签来简化前台开发。

采用临时表单提交,CSS使用了Bootstrap。

Html代码   收藏代码
  1. <%@tag pageEncoding="UTF-8"%>  
  2. <%@ attribute name="page" type="cn.com.intasect.ots.common.utils.Page" required="true"%>  
  3. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>  
  4.   
  5. <%  
  6. int current =  page.getPageNo();  
  7. int begin = 1;  
  8. int end = page.getTotalPage();  
  9.   
  10. request.setAttribute("current", current);  
  11. request.setAttribute("begin", begin);  
  12. request.setAttribute("end", end);  
  13. request.setAttribute("pList", page.getPageNoDisp());  
  14.   
  15. %>  
  16. <script type="text/javascript">  
  17.   var paras = '<%=page.getParaJson()%>';  
  18.   var paraJson = eval('(' + paras + ')');  
  19.   
  20.   //将提交参数转换为JSON  
  21.   var paraLists = '<%=page.getParaListJson()%>';  
  22.   var paraListJson = eval('(' + paraLists + ')');  
  23.   function pageClick( pNo ){  
  24.     paraJson["pageNo"] = pNo;  
  25.     paraJson["pageSize"] = "<%=page.getPageSize()%>";  
  26.       
  27.     var jsPost = function(action, values, valueLists) {  
  28.       var id = Math.random();  
  29.       document.write('<form id="post' + id + '" name="post'+ id +'" action="' + action + '" method="post">');  
  30.       for (var key in values) {  
  31.         document.write('<input type="hidden" name="' + key + '" value="' + values[key] + '" />');  
  32.       }  
  33.       for (var key2 in valueLists) {  
  34.         for (var index in valueLists[key2]) {  
  35.           document.write('<input type="hidden" name="' + key2 + '" value="' + valueLists[key2][index] + '" />');  
  36.         }  
  37.       }  
  38.       document.write('</form>');      
  39.       document.getElementById('post' + id).submit();  
  40.     }  
  41.       
  42.     //发送POST  
  43.     jsPost("<%=page.getSearchUrl()%>", paraJson, paraListJson);  
  44.   }  
  45. </script>  
  46. <div class="page-pull-right">  
  47.   <% if (current!=1 && end!=0){%>  
  48.     <button class="btn btn-default btn-sm" onclick="pageClick(1)">首页</button>  
  49.     <button class="btn btn-default btn-sm" onclick="pageClick(${current-1})">前页</button>  
  50.   <%}else{%>  
  51.     <button class="btn btn-default btn-sm" >首页</button>  
  52.     <button class="btn btn-default btn-sm" >前页</button>  
  53.   <%} %>  
  54.   <c:forTokens items="${pList}" delims="|" var="pNo">  
  55.     <c:choose>  
  56.       <c:when test="${pNo == 0}">  
  57.         <label style="font-size: 10px; width: 20px; text-align: center;">•••</label>  
  58.       </c:when>  
  59.       <c:when test="${pNo != current}">  
  60.         <button class="btn btn-default btn-sm" onclick="pageClick(${pNo})">${pNo}</button>  
  61.       </c:when>  
  62.       <c:otherwise>  
  63.         <button class="btn btn-primary btn-sm" style="font-weight:bold;">${pNo}</button>  
  64.       </c:otherwise>  
  65.     </c:choose>  
  66.   </c:forTokens>  
  67.   <% if (current<end && end!=0){%>  
  68.     <button class="btn btn-default btn-sm" onclick="pageClick(${current+1})">后页</button>  
  69.     <button class="btn btn-default btn-sm" onclick="pageClick(${end})">末页</button>  
  70.   <%}else{%>  
  71.     <button class="btn btn-default btn-sm">后页</button>  
  72.     <button class="btn btn-default btn-sm">末页</button>  
  73.   <%} %>  
  74. </div>  

注意“信使”在这里使出了浑身解数,7个主要的get方法全部用上了。

Java代码   收藏代码
  1. page.getPageNo()        //当前页号  
  2. page.getTotalPage()     //总页数  
  3. page.getPageNoDisp()    //可以显示的页号  
  4. page.getParaJson()      //查询条件  
  5. page.getParaListJson()  //数组查询条件  
  6. page.getPageSize()      //每页行数  
  7. page.getSearchUrl()     //Url地址(作为action名称)  

到这里三个核心模块完成了。然后是拦截器的注册。

 

【拦截器的注册】

需要在mybatis-config.xml 中加入拦截器的配置

Java代码   收藏代码
  1. <plugins>  
  2.    <plugin interceptor="cn.com.dingding.common.utils.PageInterceptor">    
  3.    </plugin>  
  4. </plugins>    

 

【相关代码修改】

首先是后台代码的修改,Controller层由于涉及到查询条件,需要修改的内容较多。

1)入参需增加 pageNo,pageSize 两个参数

2)根据pageNo,pageSize 及你的相对url构造page对象。(

3)最重要的是将你的其他入参(查询条件)保存到page中

4)Service层的方法需要带着page这个对象(最终目的是传递到sql执行的入参,让拦截器识别出该sql需要分页,同时传递页号)

5)将page对象传回Mode中

修改前

Java代码   收藏代码
  1. @RequestMapping(value = "/user/users")  
  2. public String list(  
  3.   @ModelAttribute("name") String name,  
  4.   @ModelAttribute("levelId") String levelId,  
  5.   @ModelAttribute("subjectId") String subjectId,  
  6.   Model model) {  
  7.   model.addAttribute("users",userService.selectByNameLevelSubject(  
  8.           name, levelId, subjectId));  
  9.   return USER_LIST_JSP;  
  10. }  

 修改后

Java代码   收藏代码
  1. @RequestMapping(value = "/user/users")  
  2. public String list(  
  3.   @RequestParam(required = false, defaultValue = "1"int pageNo,  
  4.   @RequestParam(required = false, defaultValue = "5"int pageSize,  
  5.   @ModelAttribute("name") String name,  
  6.   @ModelAttribute("levelId") String levelId,  
  7.   @ModelAttribute("subjectId") String subjectId,  
  8.   Model model) {  
  9.   // 这里是“信使”诞生之地,一出生就加载了很多重要信息!  
  10.   Page page = Page.newBuilder(pageNo, pageSize, "users");  
  11.   page.getParams().put("name", name);           //这里再保存查询条件  
  12.   page.getParams().put("levelId", levelId);  
  13.   page.getParams().put("subjectId", subjectId);  
  14.       
  15.   model.addAttribute("users",userService.selectByNameLevelSubject(  
  16.           name, levelId, subjectId, page));  
  17.   model.addAttribute("page", page);             //这里将page返回前台  
  18.   return USER_LIST_JSP;  
  19. }  

注意pageSize的缺省值决定该分页的每页数据行数 ,实际项目更通用的方式是使用配置文件指定。

 

Service层

拦截器可以自动识别在Map或Bean中的Page对象。

如果使用Bean需要在里面增加一个page项目,Map则比较简单,以下是例子。

Java代码   收藏代码
  1. @Override  
  2. public List<UserDTO> selectByNameLevelSubject(String name, String levelId, String subjectId, Page page) {  
  3.   Map<String, Object> map = Maps.newHashMap();  
  4.   levelId = DEFAULT_SELECTED.equals(levelId)?null: levelId;  
  5.   subjectId = DEFAULT_SELECTED.equals(subjectId)?null: subjectId;  
  6.   if (name != null && name.isEmpty()){  
  7.     name = null;  
  8.   }  
  9.   map.put("name", name);  
  10.   map.put("levelId", levelId);  
  11.   map.put("subjectId", subjectId);  
  12.   map.put("page", page);             //MAP的话加这一句就OK  
  13.   return userMapper.selectByNameLevelSubject(map);  
  14. }  

 

前台页面方面,由于使用了标签,在适当的位置加一句就够了。

Html代码   收藏代码
  1. <tags:page page="${page}"/>  

 “信使”page在这里进入标签,让分页按钮最终展现。

 

至此,无需修改一句sql,完成分页自动化。

 

【效果图】

 

【总结】

 现在回过头来看下最开始提出的几个问题:

1)分页时是要随时带有最近一次查询条件

  回答:在改造Controller层时,通过将提交参数设置到 Page对象的 Map<String, String> params(单个基本型参数) 和 Map<String, List<String>> 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/blog/2001142

===================================== 分割线 (1/23)=======================================

再次改善,使用ThreadLocal类封装Page对象,让Service层等无需传Page对象,减小了侵入性。拦截器也省去了查找Page对象的动作,性能也同时改善。整体代码改动不大。

===================================== 分割线 (2/21)=======================================

今天比较闲,顺便聊下这个分页的最终版,当然从来只有不断变化的需求,没有完美的方案,这里所说的最终版其实是一个优化后的“零侵入”的方案。为避免代码混乱还是只介绍思路。在上一个版本(1/23版)基础上有两点改动:

一是增加一个配置文件,按Url 配置初始的每页行数。如下面这样(pagesize 指的是每页行数):

Xml代码   收藏代码
  1. <pager url="/user/users" pagesize="10" />  

 二是增加一个过滤器,并将剩下的位于Control类中 唯一侵入性的分页相关代码移入过滤器。发现当前的 Url  在配置文件中有匹配是就构造Page对象,并加入到Response中。

 

使用最终版后,对于开发者需要分页时,只要在配置文件中加一行,并在前端页面上加一个分页标签即可,其他代码,SQL等都不需要任何改动,可以说简化到了极限。

 

【技术列表】

总结下最终方案用到的技术:

  • Mybatis 提供的拦截器接口实现(实现分页sql自动 select count 及limit 拼接)
  • Servlet过滤器+ThreadLocal  (生成线程共享的Page对象)
  • 标签文件   (实现前端共通的分页效果)
  • 临时表单提交 (减少页面体积)

【其他分页方案比较】

时下比较成熟的 JPA 的分页方案,(主要应用在 Hibernate + Spring Data 的场合),主要切入点在DAO层,而Controller等各层接口依然需要带着pageNumber,pageSize 这些的参数,另外框架开发者还要掌握一些必须的辅助类,如:

  org.springframework.data.repository.PagingAndSortingRepository    可分页DAO基类

  org.springframework.data.domain.Page            抽取结果封装类

  org.springframework.data.domain.Pageable     分页信息类

比较来看 本方案 做到了分页与业务逻辑的完全解耦,开发者无需关注分页,全部通过配置实现。通过这个例子也可以反映出Mybatis在底层开发上有其独特的优势。

 

【备选方案】

最后再闲扯下,上面的最终案是基于 Url 配置的,其实也可以基于方法加自定义注解来做。这样配置文件省了,但是要增加一个注解解析类。注解中参数 为初始的每页行数。估计注解fans会喜欢,如下面的样子:

Java代码   收藏代码
  1. @RequestMapping(value = "/user/users")  
  2. @Pagination(size=10)  
  3. public String list(  
  4. ...  

同样与过滤器配合使用,只是注解本身多少还是有“侵入性”。在初始行数基本不会变更时,这个比较直观的方案也是不错的选择。大家自行决定吧。


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

分享一个完整的Mybatis分页解决方案 的相关文章

  • 在 Spring Webflux 中执行阻塞 JDBC 调用

    我使用 Spring Webflux 和 Spring data jpa 使用 PostgreSql 作为后端数据库 我不想在进行数据库调用时阻塞主线程 例如find and save 为了实现同样的目标 我有一个主调度程序Controll
  • Java:将二维字符串数组打印为右对齐表格

    是什么best打印a的单元格的方法String 数组作为右对齐表 例如 输入 x xxx yyy y zz zz 应该产生输出 x xxx yyy y zz zz 这似乎是一个should能够完成使用java util Formatter
  • ORDER BY id 或 date_created 显示最新结果?

    我有一个表 实际上有几个 我想首先从中获取最新条目的结果 这是我的ORDER BY条款选项 date created INT 从不改变值 id 当然是INT AUTO INCRMENT 两列应同等地代表记录插入的顺序 我自然会使用date
  • grails 中的 log4j:如何登录文件?

    我的 grails config groovy 中有这个 log4j 配置 log4j error org codehaus groovy grails web servlet controllers org codehaus groovy
  • 从 datagridview 选定的行更新 mysql 数据库

    我有一个 datagridview 它在表单加载事件上加载 mysql 数据库表 t pi clients 并且我有另一个选项卡 其中包含 t pi client 相应列的文本框 它能够从 fullrowselect 模式获取数据到这些文本
  • 子查询与连接

    我重构了从另一家公司继承的应用程序的一个缓慢部分 以使用内部联接而不是子查询 例如 WHERE id IN SELECT id FROM 重构后的查询运行速度提高了约 100 倍 50 秒到 0 3 我预计会有改进 但谁能解释为什么它如此剧
  • 如何列出表中的所有列?

    对于各种流行的数据库系统 如何列出表中的所有列 对于 MySQL 请使用 DESCRIBE name of table 只要您使用 SQL Plus 或 Oracle 的 SQL Developer 这也适用于 Oracle
  • php echo 不工作

    我的代码似乎不起作用 单选按钮出现 但旁边什么也没有 似乎 mysql fetch array 由于某种原因无法工作 因为我已经玩过代码并反复测试它以查找代码似乎遇到的位置出现问题并停止工作 有人可以告诉我出了什么问题吗 欢呼声我是新手 最
  • SwingUtilities.invokeLater

    我的问题与SwingUtilities invokeLater 我应该什么时候使用它 每次需要更新 GUI 组件时都必须使用吗 它到底有什么作用 是否有替代方案 因为它听起来不直观并且添加了看似不必要的代码 Do I have to use
  • Java MYSQL/JDBC 查询从缓存的连接返回过时的数据

    我一直在 Stackoverflow 中寻找答案 但似乎找不到不涉及 Hibernate 或其他数据库包装器的答案 我直接通过 Tomcat 6 Java EE 应用程序中的 MYSQL 5 18 JDBC 驱动程序使用 JDBC 我正在缓
  • 覆盖Java中的属性[重复]

    这个问题在这里已经有答案了 在 Java 中 我最近有几个项目 我使用了这样的设计模式 public abstract class A public abstract int getProperty public class B exten
  • 运行 Espresso 测试时在 Android studio 中找不到属性 android:forceQueryable

    我已经使用 android studio 录制了我的 Android 应用程序 Espresso 测试记录浓缩咖啡测试选项中Run菜单 在记录的最后 我用自己的文件名保存了测试 单击保存按钮后 IDE 会自动在以下位置创建文件Android
  • 使用 Retrofit 获取原始 HTTP 响应

    我想从我的 API REST 获取原始 http 响应 我尝试过这个界面 POST login FormUrlEncoded Call
  • 可空日期列合并问题

    我在 Geronimo 应用程序服务器上使用 JPA 和下面的 openjpa 实现 我也在使用MySQL数据库 我在更新具有可为空 Date 属性的对象时遇到问题 当我尝试合并 Date 属性设置为 null 的实体时 不会生成 sql
  • 隐藏 JTable 临时列

    我正在使用 JTable 显示数据库中的数据 现在我想通过 Jcombobox 过滤我的 jtable 我正在使用 Jcombo 框 其中包含 030 024 045 等值 这些值已在 jtable 中设置为列标题 当我单击组合时 选定的列
  • Zookeeper 未启动,nohup 错误

    我已经下载了zookeeper 3 4 5 tar gz 解压后我将conf zoo cfg写为 tickTime 2000 dataDir var zookeeper clientPort 2181 现在我尝试通过 bin zkServe
  • Bipush 在 JVM 中如何工作?

    我知道 iload 接受整数 1 到 5 但是如何使用 bipush 指令扩展到更高的数字 特定整数如何与字节码一起存储 有几种不同的指令可用于推送整数常量 最小的是iconst 指令 这些只是一个字节 因为该值是在操作码本身中编码的 ic
  • com.fasterxml.jackson.databind.JsonMappingException:无法反序列化 org.springframework.data.domain.Sort 的实例 START_ARRAY 令牌

    我的 objectMapper 正在获取类型的值Page
  • 如何在 tomcat 上部署 Java Web 应用程序 (.war)?

    我有一个 warJava Web 应用程序的文件 现在我想将它上传到我的 ftp 服务器 以便我可以执行它 我应该执行哪些步骤来运行它 webapp的上下文路径是 mywebapp Edit 实际上 我的 ftp 服务器名称是ftp bil
  • ImageIO.read(...) - 非常慢,有更好的方法吗?

    我正在加载大量将在我的应用程序中使用的图标 我计划在服务器启动时从 jar 中加载所有这些 然而 由于数百张图像加起来刚刚超过 9MB 执行此任务仍然需要 30 秒多的时间 我现在正在一个单独的线程中执行此操作 但这让我想知道我是否在代码中

随机推荐

  • Pytorch学习1-GRU使用和参数说明

    import torch nn as nn import torch gru nn GRU input size 50 hidden size 50 batch first True embed nn Embedding 3 50 x to
  • Redis GEO 的java实现(通过Jedis)(GIS相关)

    1 Redis的安装参考 https blog csdn net zhu tianwei article details 44890579 2 Jedis使用2 90版本 maven配置
  • Python Selenium库的使用【从安装到实战】

    Selenium Selenium简介 Selenium 的安装 Selenium基础操作 定位UI元素 鼠标动作链 键盘常用 下拉列表 填充表单 非select元素 鼠标悬浮 以后展现 select元素 其他一些常用功能 弹窗处理 页面切
  • pyqt5的QWebEngineView无法播放网络视频

    这个问题是2021年中旬遇到的 最近系统重装又出现这个问题所以解决并记录一下 具体就是可以实现打开网页 但是如果是视频链接就无法播放 例如 做类似抖音 快手网页版的视频播放器 或者其他类型的视频播放器等等如封面图 都会遇到只有黑窗口但无法播
  • 全网最火Java面试题

    第一部分 JAVA 基础 第一节 IO NIO 第二节 反射 第三节 多线程 第四节 集合 第五节 Web 第六节 其他 第七节 关键字 第八节 操作符 第九节 基础类型 第十节 异常 第十一节 JDBC 第十二节 OOP 第二部分 JVM
  • 深入理解Qt4/Qt5信号和槽机制

    对于事件处理 MFC中使用的是消息映射机制 Qt使用的是信号和槽机制 在我看来 Qt的信号和槽比MFC功能更强大 也更灵活 1 信号和槽的简单介绍 一般格式 connect Sender SIGNAL signal Receiver SLO
  • C++派生类的不同继承方式对基类的访问权限

    经过我细心的整理 形成了这张表 一张表说明派生类的不同继承方式 对基类的访问权限 总的来说 对类的访问权限范围public
  • 2022 CISCN初赛 Satool

    一个2022年国赛初赛的LLVM PASS类pwn题 当时还完全没有接触过 所以直接放弃掉了 初赛结束之后决定入门一下这方面知识 看这篇题解之前最好先看看之前写的这篇入门文章 LLVM PASS类pwn题入门 然后我们正式开始这道题 首先从
  • 07-js 逆向-返回数据加密(aes)

    目标 返回的结果有加密 把结果解密 可以看到返回来的data是加密的 但是加密的数据并没有进行混淆 这时候我们可以采用直接搜解密 decrypt 直接发先我们的数据书通过aes加密的 我们开始些python代码 from Crypto Ci
  • vndk: (native:vendor) should not link to libcamera_client (native:platform)

    1 0 相似例子 2 21 17 47 30 305 4365 4365 E CamX ERROR UTILS camxosutilslinux cpp 874 LibMap dlopen dlopen failed library lib
  • 利用mimikatz查看rdp连接密码【渗透测试】

    0x00 概述 在使用 rdp 时会发现系统有保存连接密码的功能 一定在本地以一种加密方式保存 在连接的时候解密进行rdp尝试 那么我们能不能那到加密的密码解密以获取这台机器rdp连接过的机器呢 0x01 流程 AppData Local
  • PUMA:DOA估计模式的改进实现(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 Matlab代码实现 4 参考文献 1 概述 文献来源 下载链接 PUMA An Imp
  • ue4添加第三方库

    查了一些资料 发现最后都是用loadlibrary的方式 这样很不方便 如果有10000个函数 要写10000次么 仔细想想 调用第三方库无非就是把头文件和lib库设置下 把相应的 h lib和 dll放到相应的位置 再在调用的地方包含头文
  • cadence原理图封装pin名称重复_Cadence原理图库文件引脚名重复处理方法介绍

    立题简介 内容 Cadence原理图库文件引脚名重复处理方法 来源 实际使用得出 作用 介绍2种处理Cadence原理图库文件引脚名的方法 PCB环境 Cadence 16 6 orCAD环境 日期 2019 03 09 分割线 立题详解
  • spring打印http接口请求和响应

    在程序日志中打印出接口请求和响应的内容是一个基本的技术需求 如果在每个接口中实现请求响应的日志打印 程序编写会很繁琐 我们可以利用spring提供的机制 集中处理接口请求响应的日志打印 具体的代码参照 示例项目 https github c
  • 使用ipmitool命令检测电源模块状态

    1 通过ipmitool检查电源模块状态 https mp weixin qq com s Z1g79Q1aMhOT9Xm9fvIkjg 2 通过ipmitool获取服务器各元件温度信息 https mp weixin qq com s E
  • 大数据分布式计算开源框架Hadoop的介绍和运用

    Hadoop是Apache开源组织的一个分布式计算开源框架 在很多大型网站上都已经得到了应用 如亚马逊 Facebook和Yahoo等等 对于我来说 最近的一个使用点就是服务集成平台的日志分析 服务集成平台的日志量将会很大 而这也正好符合了
  • vue 快速自定义分页el-pagination

    vue 快速自定义分页el pagination template div style text align center div
  • main函数中的参数代表的意义

    int main int argc char argv 或者是 int main int argc char argv 里面的参数是什么意义呢 argc 是 argument count的缩写 表示传入main函数的参数个数 argv 是
  • 分享一个完整的Mybatis分页解决方案

    原文地址 http duanhengbin iteye com blog 1998017 参考地址 http blog csdn net isea533 article details 23831273 Mybatis 的物理分页是应用中的