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

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

我在最近的项目中开发了这个通用分页器,过程中参考了站内不少好文章,阅读源码帮助更大minglisoft.cn/technology

【背景】

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

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

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

采用临时表单提交,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. int current =  page.getPageNo();
  6. int begin = 1;
  7. int end = page.getTotalPage();
  8. request.setAttribute("current", current);
  9. request.setAttribute("begin", begin);
  10. request.setAttribute("end", end);
  11. request.setAttribute("pList", page.getPageNoDisp());
  12. %>
  13. <script type="text/javascript">
  14. var paras = ‘<%=page.getParaJson()%>‘;
  15. var paraJson = eval(‘(‘ + paras + ‘)‘);
  16. //将提交参数转换为JSON
  17. var paraLists = ‘<%=page.getParaListJson()%>‘;
  18. var paraListJson = eval(‘(‘ + paraLists + ‘)‘);
  19. function pageClick( pNo ){
  20. paraJson["pageNo"] = pNo;
  21. paraJson["pageSize"] = "<%=page.getPageSize()%>";
  22. var jsPost = function(action, values, valueLists) {
  23. var id = Math.random();
  24. document.write(‘<form id="post‘ + id + ‘" name="post‘+ id +‘" action="‘ + action + ‘" method="post">‘);
  25. for (var key in values) {
  26. document.write(‘<input type="hidden" name="‘ + key + ‘" value="‘ + values[key] + ‘" />‘);
  27. }
  28. for (var key2 in valueLists) {
  29. for (var index in valueLists[key2]) {
  30. document.write(‘<input type="hidden" name="‘ + key2 + ‘" value="‘ + valueLists[key2][index] + ‘" />‘);
  31. }
  32. }
  33. document.write(‘</form>‘);
  34. document.getElementById(‘post‘ + id).submit();
  35. }
  36. //发送POST
  37. jsPost("<%=page.getSearchUrl()%>", paraJson, paraListJson);
  38. }
  39. </script>
  40. <div class="page-pull-right">
  41. <% if (current!=1 && end!=0){%>
  42. <button class="btn btn-default btn-sm" onclick="pageClick(1)">首页</button>
  43. <button class="btn btn-default btn-sm" onclick="pageClick(${current-1})">前页</button>
  44. <%}else{%>
  45. <button class="btn btn-default btn-sm" >首页</button>
  46. <button class="btn btn-default btn-sm" >前页</button>
  47. <%} %>
  48. <c:forTokens items="${pList}" delims="|" var="pNo">
  49. <c:choose>
  50. <c:when test="${pNo == 0}">
  51. <label style="font-size: 10px; width: 20px; text-align: center;">???</label>
  52. </c:when>
  53. <c:when test="${pNo != current}">
  54. <button class="btn btn-default btn-sm" onclick="pageClick(${pNo})">${pNo}</button>
  55. </c:when>
  56. <c:otherwise>
  57. <button class="btn btn-primary btn-sm" style="font-weight:bold;">${pNo}</button>
  58. </c:otherwise>
  59. </c:choose>
  60. </c:forTokens>
  61. <% if (current<end && end!=0){%>
  62. <button class="btn btn-default btn-sm" onclick="pageClick(${current+1})">后页</button>
  63. <button class="btn btn-default btn-sm" onclick="pageClick(${end})">末页</button>
  64. <%}else{%>
  65. <button class="btn btn-default btn-sm">后页</button>
  66. <button class="btn btn-default btn-sm">末页</button>
  67. <%} %>
  68. </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. model.addAttribute("users",userService.selectByNameLevelSubject(
  15. name, levelId, subjectId, page));
  16. model.addAttribute("page", page);             //这里将page返回前台
  17. return USER_LIST_JSP;
  18. }

注意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. ...

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

详细参考源码来源minglisoft.cn/technology

时间: 2024-10-25 09:09:24

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

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

[布景] 号码大全项目结构是 SpringMVC+Mybatis, 需求是想选用自定义的分页标签,关键词挖掘工具一起,要尽量少的影响事务程序开发的. 假如你已经运用了JS结构( 如:Ext,EasyUi等)自带的分页机能,这篇文章帮助能够不大,因为JS结构供给了固定的接口. [关于疑问] 大多数分页器会运用在查询页面,要思考以下疑问: 1)分页时是要随时带有近来一次查询条件 2)不能影响现有的sql,相似aop的作用 3)mybatis供给了通用的阻拦接口,要挑选恰当的阻拦办法和时点 4)尽量少

很好用的mybatis分页解决方案

分页如果写在SQL脚本中,将会大大影响我们后续数据库的迁移难度.mybatis的分页一般是自己实现一个mybatis的拦截器,然后根据某些特定的条件开启分页,对原有SQL进行改造. 正在我对mybatis的拦截器进行研究的时候从网上找到了一个很好的分页插件,主页地址是 https://github.com/pagehelper/Mybatis-PageHelper 已经实现了拦截器/多种数据库的适配 基本上很方便的即可集成到我们的项目中.集成方式: 1.使用maven引入jar包: <depen

Mybatis分页解决方案

转自:  http://blog.csdn.net/fover717/article/details/8334209 http://www.cnblogs.com/zemliu/archive/2013/08/07/3242966.html http://fhd001.iteye.com/blog/1121189 参考:http://blog.csdn.net/isea533/article/details/23831273 http://blog.csdn.net/hupanfeng/arti

一个完整的mybatis项目,包含增删改查

1.导入jar包,导入相关配置文件,均在自己博客园的文件中 编写mybatis.xml文件 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "./mybatis-3-config.dtd"> <configuration> &l

分享一个VS2013代码窗口一闪而过的解决方案。

下载完VS2013,写一个简单代码以后,我遇到了运行窗口一闪而过的情况,我按Ctrl+F5也没能杜绝这个情况发生. 解决方案:上面一栏"项目-属性-配置属性-链接器-系统-子系统",点击"子系统"左边,选择"/SUBSYSTEM:CONSOLE". 然后按"确定"后,按Ctrl+F5即可解决. 借鉴:老师指导和百度. 感谢阅读.

福利到~分享一个基于jquery的分页控件

前台分页控件有很多,以下是我的实现.默认情况下,点击页码会像博客园一样,在url后面加上"#p页码". 有2个参数需要注意: beforeRender: 在每个页码项呈现前会被调用,参数为页码的jQuery对象.这个时候我们可以在呈现前做一些处理,例如增加自己的属性等.默认情况下,点击页码,会在url后面加上“#p页码”,这样的url并不会刷新页面.如果我们需要刷新页面,例如url为,"default.aspx?index=页码",就可以在这个回调函数里处理. ca

【Android】 分享一个完整的项目,适合新手!

写这个app之前是因为看了头条的一篇文章:http://www.managershare.com/post/155110,然后心想要不做一个这样的app,让手机计算就行了.也就没多想就去开始整了. 项目用到了三个开源包: 一个是图片加载:https://github.com/nostra13/Android-Universal-Image-Loader 使用方法: 1.在Appliction的onCreate里初始化 /** * 初始化imageLoader */ public void ini

分享一个通用的分页SQL

又很久没写博客,今天记录一个SQLserver通用分页存储过程(适用于SqlServer2000及以上版本) 1.支持连表 2.支持条件查询 USE [MYDB] GO /****** Object:  StoredProcedure [dbo].[SP_CommonPage] SET QUOTED_IDENTIFIER ON GO ------------------------------------ --用途:分页存储过程(对有主键的表效率极高) --说明: ---------------

一个典型的PHP分页实例代码分享

一个典型的PHP分页实例代码分享,学习php的朋友肯定用得到,主要是了解思路. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <hea