1. 程式人生 > >分享一個完整的Mybatis分頁解決方案

分享一個完整的Mybatis分頁解決方案

Mybatis 的物理分頁是應用中的一個難點,特別是配合檢索和排序功能疊加時更是如此。

我在最近的專案中開發了這個通用分頁器,過程中參考了站內不少好文章,新年第一天,特此發文回饋網站。

【背景】

專案框架是 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. publicclass Page {  
  13.   private
    staticfinal Logger logger = LoggerFactory.getLogger(Page.class);  
  14.   privatestatic ObjectMapper mapper = new ObjectMapper();  
  15.   publicstatic String DEFAULT_PAGESIZE = "10";  
  16.   privateint pageNo;          //當前頁碼
  17.   privateint pageSize;        //每頁行數
  18.   privateint totalRecord;      //總記錄數
  19.   privateint 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.   publicstatic 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.   privatevoid 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.     }elseif ( 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.     }elseif ( 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.   publicint getPageNo() {  
  131.      return pageNo;  
  132.   }  
  133.   publicvoid setPageNo(int pageNo) {  
  134.      this.pageNo = pageNo;  
  135.   }  
  136.   publicint getPageSize() {  
  137.      return pageSize;  
  138.   }  
  139.   publicvoid setPageSize(int pageSize) {  
  140.      this.pageSize = pageSize;  
  141.   }  
  142.   publicint getTotalRecord() {  
  143.      return totalRecord;  
  144.   }  
  145.   publicvoid setTotalRecord(int totalRecord) {  
  146.     this.totalRecord = totalRecord;  
  147.     refreshPage();       
  148.   }  
  149.   publicint getTotalPage() {  
  150.      return totalPage;  
  151.   }  
  152.   publicvoid setTotalPage(int totalPage) {  
  153.      this.totalPage = totalPage;  
  154.   }  
  155.   public Map<String, String> getParams() {  
  156.      return params;  
  157.   }  
  158.   publicvoid setParams(Map<String, String> params) {  
  159.      this.params = params;  
  160.   }  
  161.   public Map<String, List<String>> getParamLists() {  
  162.     return paramLists;  
  163.   }  
  164.   publicvoid setParamLists(Map<String, List<String>> paramLists) {  
  165.     this.paramLists = paramLists;  
  166.   }  
  167.   public String getSearchUrl() {  
  168.     return searchUrl;  
  169.   }  
  170.   publicvoid setSearchUrl(String searchUrl) {  
  171.     this.searchUrl = searchUrl;  
  172.   }  
  173.   public String getPageNoDisp() {  
  174.     return pageNoDisp;  
  175.   }  
  176.   publicvoid 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. publicclass 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.     returnnull;  
  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.   publicclass 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.   publicvoid 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. <scripttype="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('<formid="post' + id + '"name="post'+ id +'"action="' + action + '"method="post">');  
  25.       for (var key in values) {  
  26.         document.write('<inputtype="hidden"name="' + key + '"value="' + values[key] + '"/>');  
  27.       }  
  28.       for (var key2 in valueLists) {  
  29.         for (var index in valueLists[key2]) {  
  30.           document.write('<inputtype="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. <divclass="page-pull-right">
  41.   <% if (current!=1 && end!=0){%>
  42.     <buttonclass="btn btn-default btn-sm"onclick="pageClick(1)">首頁</button>
  43.     <buttonclass="btn btn-default btn-sm"onclick="pageClick(${current-1})">前頁</button>
  44.   <%}else{%>
  45.     <buttonclass="btn btn-default btn-sm">首頁</button>
  46.     <buttonclass="btn btn-default btn-sm">前頁</button>
  47.   <%} %>
  48.   <c:forTokensitems="${pList}"delims="|"var="pNo">
  49.     <c:choose>
  50.       <c:whentest="${pNo == 0}">
  51.         <labelstyle="font-size: 10px; width: 20px; text-align: center;">•••</label>
  52.       </c:when>
  53.       <c:whentest="${pNo != current}">
  54.         <buttonclass="btn btn-default btn-sm"onclick="pageClick(${pNo})">${pNo}</button>
  55.       </c:when>
  56.       <c:otherwise>
  57.         <buttonclass="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.     <buttonclass="btn btn-default btn-sm"onclick="pageClick(${current+1})">後頁</button>
  63.     <buttonclass="btn btn-default btn-sm"onclick="pageClick(${end})">末頁</button>
  64.   <%}else{%>
  65.     <buttonclass="btn btn-default btn-sm">後頁</button>
  66.     <buttonclass="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:pagepage="${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. <pagerurl="/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. ...  

同樣與過濾器配合使用,只是註解本身多少還是有“侵入性”。在初始行數基本不會變更時,這個比較直觀的方案也是不錯的選擇。大家自行決定吧。