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类对象会以“信使”的身份出现在全部与分页相关的地方。

  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 包,做对象查询。

  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。

  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方法全部用上了。

  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 中加入拦截器的配置

  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中

修改前

  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. }

修改后

  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则比较简单,以下是例子。

  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. }

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

  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 指的是每页行数):

  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会喜欢,如下面的样子:

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

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

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

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

  1. 很好用的mybatis分页解决方案

    分页如果写在SQL脚本中,将会大大影响我们后续数据库的迁移难度.mybatis的分页一般是自己实现一个mybatis的拦截器,然后根据某些特定的条件开启分页,对原有SQL进行改造. 正在我对mybat ...

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

    1.导入jar包,导入相关配置文件,均在自己博客园的文件中 编写mybatis.xml文件 <?xml version="1.0" encoding="UTF-8& ...

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

    写这个app之前是因为看了头条的一篇文章:http://www.managershare.com/post/155110,然后心想要不做一个这样的app,让手机计算就行了.也就没多想就去开始整了.   ...

  4. MongoDB + Spark: 完整的大数据解决方案

    Spark介绍 按照官方的定义,Spark 是一个通用,快速,适用于大规模数据的处理引擎. 通用性:我们可以使用Spark SQL来执行常规分析, Spark Streaming 来流数据处理, 以及 ...

  5. 分享一个手机端好用的jquery ajax分页类

    分享一个手机端好用的jquery ajax分页类 jquery-ias.min.js 1,引入jquery-ias.min.js 2,调用ajax分页 <script type="te ...

  6. 分享一个.NET(C#)按指定字母个数截断英文字符串的方法–提供枚举选项,可保留完整单词

    分享一个.NET(C#)按字母个数截断英文字符串的方法,该方法提供枚举选项.枚举选项包括:可保留完整单词,允许最后一个单词超过最大长度限制,字符串最后跟省略号以及不采取任何操作等,具体示例实现代码如下 ...

  7. mybatis分页的一种解决方案

    mybatis自定义分页解决方案  1.PageSqlProvider<T> —— 提供默认的分页列表查询 package com.xinyartech.erp.core.base; im ...

  8. 分享一个漂亮的ASP.NET MVC界面框架

    本文分享一个插件化的界面框架,该框架提供了用户.角色.权限管理功能,也提供了插件的管理和插件中心.下图是该界面框架的样式(全部源码和原理介绍下一篇分享,推荐越多,源码放的越早,呵呵). 要使用该界面框 ...

  9. SSM 使用 mybatis 分页插件 pagehepler 实现分页

    使用分页插件的原因,简化了sql代码的写法,实现较好的物理分页,比写一段完整的分页sql代码,也能减少了误差性. Mybatis分页插件 demo 项目地址:https://gitee.com/fre ...

随机推荐

  1. Android -- 自定义ViewGroup+贝塞尔+属性动画实现仿QQ点赞效果

    1,昨天我们写了篇简单的贝塞尔曲线的应用,今天和大家一起写一个QQ名片上常用的给别人点赞的效果,实现效果图如下: 红心的图片比较丑,见谅见谅(哈哈哈哈哈哈).... 2,实现的思路和原理 从上面的效果 ...

  2. 1137: 零起点学算法44——多组测试数据输出II

    1137: 零起点学算法44--多组测试数据输出II Time Limit: 1 Sec  Memory Limit: 64 MB   64bit IO Format: %lldSubmitted: ...

  3. java 基础知识八 正则表达式

    java  基础知识八  正则表达式 正则表达式是一种可以用于模式匹配和替换的规范,一个正则表达式就是由普通的字符(例如字符a到z)以及特殊字符(元字符)组成的文字模式,它 用以描述在查找文字主体时待 ...

  4. 在 redhat 6.4上安装Python 2.7.5

    在工作环境中使用的是python 2.7.*,但是CentOS 6.4中默认使用的python版本是2.6.6,故需要升级版本. 安装步骤如下: 1,先安装GCC,用如下命令yum install g ...

  5. 24(java_io from keyboard)

    public class ReadFromKB{ public static void main(String args[]) { try { byte bArray[]=new byte[128]; ...

  6. ios系统判断某些适配 __IPHONE_OS_VERSION_MAX_ALLOWED

    由于app的最新设计字体是ios9之后的平方字体,但app最低支持ios7,so...想在常量配置文件类里统一适配下字体,如下: //适配字体,ios9及以上系统使用新字体--平方字体 #if __I ...

  7. 关于REST的浅显了解

    REST 是一种软件架构风格 1.定义 REST即表述性状态传递(Representational State Transfer) 是一组架构约束条件和原则.是设计风格而不是标准. 满足这些约束条件和 ...

  8. HubbleDotNet 最新绿色版,服务端免安装,基于eaglet 最后V1.2.8.9版本开发,bug修正,支持一键生成同步表

    HubbleDotNet 是一个基于.net framework 的开源免费的全文搜索数据库组件.开源协议是 Apache 2.0.HubbleDotNet提供了基于SQL的全文检索接口,使用者只需会 ...

  9. Java Comparator的范型类型推导问题

    问题 在项目中,有一处地方需要对日期区间进行排序 我需要以日期区间的开始日为第一优先级,结束日为第二优先级进行排序 代码 我当时写的代码如下: List<Pair<LocalDate, L ...

  10. 搜索结果Refinement 行为总结之 multi-selection refinement

    几乎所有的购物网站的搜索结果页面都会提供refinement (filtering) 给用户去过滤产品,以便能更快找到自己想要的产品.(做的都是国外的项目,不太清楚这个功能地道的中文名是什么.所以就暂 ...