mybatis3.1分页自动添加总数

来源:互联网 发布:excel表xy轴怎么填数据 编辑:程序博客网 时间:2024/05/31 11:05
1.mybatis默认分页是内存分页的
  类似于下面的DAO签名方法,只要有RowBounds入参,Mybatis即会自动内存分页: 
Java代码  收藏代码
  1. @Select("SELECT * FROM cartan_common.t_app s WHERE s.author = #{param.author}")  
  2. ArrayList<App> queryList(@Param("param")AppQueryParam appQueryParam,RowBounds rowBounds);  


   我们必须将其转换为物理分页,也即数据库分页。 

2.分页一般都需要自动计算出总行数,而在mybatis中,你必须手动发起两次请求,烦人。 

解决思路 

1.Mybatis的拦截器是我们动手动脚的地方 

  Mybatis的架构是非常漂亮的,它允许对多个接口的多个方法定义拦截器(Interceptor),以下是Mybatis的调用粗线: 


    我们不但可以对Excutor的方法编写插件,还可以对StatementHandler或ResultSetHandler的方法编写插件。以下是一个Mybatis的插件: 
Java代码  收藏代码
  1. package com.ridge.dao.mybatis;  
  2.   
  3. import org.apache.commons.lang.reflect.FieldUtils;  
  4. import org.apache.commons.logging.Log;  
  5. import org.apache.commons.logging.LogFactory;  
  6. import org.apache.ibatis.executor.statement.PreparedStatementHandler;  
  7. import org.apache.ibatis.executor.statement.RoutingStatementHandler;  
  8. import org.apache.ibatis.executor.statement.StatementHandler;  
  9. import org.apache.ibatis.mapping.BoundSql;  
  10. import org.apache.ibatis.mapping.ParameterMapping;  
  11. import org.apache.ibatis.plugin.*;  
  12. import org.apache.ibatis.session.Configuration;  
  13. import org.apache.ibatis.session.RowBounds;  
  14. import org.hibernate.dialect.Dialect;  
  15.   
  16. import java.sql.Connection;  
  17. import java.util.Properties;  
  18. import java.util.regex.Matcher;  
  19. import java.util.regex.Pattern;  
  20.   
  21. /** 
  22.  * 默认情况下 
  23.  * 数据库的类型 
  24.  * 
  25.  * @author : chenxh 
  26.  * @date: 13-7-5 
  27.  */  
  28. @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})  
  29. public class PaginationInterceptor implements Interceptor {  
  30.     @Override  
  31.     public Object intercept(Invocation invocation) throws Throwable {  
  32.        ...  
  33.     }  
  34. }  

    注意PaginationInterceptor类的@Intercepts注解,如上所示,它将对StatementHandler的prepare(Connection connection)方法进行拦截。 

2.怎样将mybatis的语句转换为分页的语句呢 
   
    这得求助Hibernate的org.hibernate.dialect.Dialect接口及其实现类。我们知道不同数据库分页的方法是不一样的。mysql是limit x,y,而Oracle要用一个子查询,使用rownum来做到。Hibernater的org.hibernate.dialect.Dialect的以下方法可以非常方便地让我们做到这点: 
Java代码  收藏代码
  1. getLimitString(originalSql, rowBounds.getOffset(), rowBounds.getLimit())  

   以下就是使用该方法完成的转换: 
引用

SELECT * FROM cartan_common.t_app s WHERE s.author = ? 
对应的分页SQL: 
SELECT * FROM cartan_common.t_app s WHERE s.author = ? limit ?, ? 


3.怎样生成SQL对应的总条数SQL呢?   

   通过以下的类的MybatisHepler.getCountSql(originalSql)方法即可: 
Java代码  收藏代码
  1. package com.ridge.dao.mybatis;  
  2.   
  3.   
  4. import com.ridge.dao.Paging;  
  5. import org.apache.ibatis.session.RowBounds;  
  6.   
  7. import java.util.regex.Matcher;  
  8. import java.util.regex.Pattern;  
  9.   
  10. public class MybatisHepler {  
  11.   
  12.     /** 
  13.      * 获取查询总数对应的SQL 
  14.      * @param querySelect 
  15.      * @return 
  16.      */  
  17.     public static String getCountSql(String querySelect) {  
  18.   
  19.         querySelect = compress(querySelect);  
  20.         int orderIndex = getLastOrderInsertPoint(querySelect);  
  21.   
  22.         int formIndex = getAfterFormInsertPoint(querySelect);  
  23.         String select = querySelect.substring(0, formIndex);  
  24.   
  25.         //如果SELECT 中包含 DISTINCT 只能在外层包含COUNT  
  26.         if (select.toLowerCase().indexOf("select distinct") != -1 || querySelect.toLowerCase().indexOf("group by") != -1) {  
  27.             return new StringBuffer(querySelect.length()).append(  
  28.                     "select count(1) count from (").append(  
  29.                     querySelect.substring(0, orderIndex)).append(" ) t")  
  30.                     .toString();  
  31.         } else {  
  32.             return new StringBuffer(querySelect.length()).append(  
  33.                     "select count(1) count ").append(  
  34.                     querySelect.substring(formIndex, orderIndex)).toString();  
  35.         }  
  36.     }  
  37.   
  38.     /** 
  39.      * 得到最后一个Order By的插入点位置 
  40.      * 
  41.      * @return 返回最后一个Order By插入点的位置 
  42.      */  
  43.     private static int getLastOrderInsertPoint(String querySelect) {  
  44.         int orderIndex = querySelect.toLowerCase().lastIndexOf("order by");  
  45.         if (orderIndex == -1) {  
  46.             orderIndex = querySelect.length();  
  47.         }else{  
  48.            if(!isBracketCanPartnership(querySelect.substring(orderIndex, querySelect.length()))){  
  49.                throw new RuntimeException("My SQL 分页必须要有Order by 语句!");  
  50.            }  
  51.         }  
  52.         return orderIndex;  
  53.     }  
  54.   
  55.     /** 
  56.      * 将{@code paging}转换为{@link org.apache.ibatis.session.RowBounds}对象 
  57.      * @param paging 
  58.      * @return 
  59.      */  
  60.     public static final RowBounds toRowBounds(Paging paging){  
  61.         return new RowBounds(paging.getRowOffset(),paging.getPageSize());  
  62.     }  
  63.   
  64.     /** 
  65.      * 得到分页的SQL 
  66.      * 
  67.      * @param offset 偏移量 
  68.      * @param limit  位置 
  69.      * @return 分页SQL 
  70.      */  
  71.     public static String getPagingSql(String querySelect, int offset, int limit) {  
  72.   
  73.         querySelect = compress(querySelect);  
  74.   
  75.         String sql = querySelect.replaceAll("[^\\s,]+\\.""") + " limit " + offset + " ," + limit;  
  76.   
  77.         return sql;  
  78.   
  79.     }  
  80.   
  81.     /** 
  82.      * 将SQL语句压缩成一条语句,并且每个单词的间隔都是1个空格 
  83.      * @param sql SQL语句 
  84.      * @return 如果sql是NULL返回空,否则返回转化后的SQL 
  85.      */  
  86.     private static String compress(String sql) {  
  87.         return sql.replaceAll("[\r\n]"" ").replaceAll("\\s{2,}"" ");  
  88.     }  
  89.   
  90.     /** 
  91.      * 得到SQL第一个正确的FROM的的插入点 
  92.      */  
  93.     private static int getAfterFormInsertPoint(String querySelect) {  
  94.         String regex = "\\s+FROM\\s+";  
  95.         Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);  
  96.         Matcher matcher = pattern.matcher(querySelect);  
  97.         while (matcher.find()) {  
  98.             int fromStartIndex = matcher.start(0);  
  99.             String text = querySelect.substring(0, fromStartIndex);  
  100.             if (isBracketCanPartnership(text)) {  
  101.                 return fromStartIndex;  
  102.             }  
  103.         }  
  104.         return 0;  
  105.     }  
  106.   
  107.     /** 
  108.      * 判断括号"()"是否匹配,并不会判断排列顺序是否正确 
  109.      * 
  110.      * @param text 要判断的文本 
  111.      * @return 如果匹配返回TRUE, 否则返回FALSE 
  112.      */  
  113.     private static boolean isBracketCanPartnership(String text) {  
  114.         if (text == null  
  115.                 || (getIndexOfCount(text, '(') != getIndexOfCount(text, ')'))) {  
  116.             return false;  
  117.         }  
  118.         return true;  
  119.     }  
  120.   
  121.     /** 
  122.      * 得到一个字符在另一个字符串中出现的次数 
  123.      * 
  124.      * @param text 文本 
  125.      * @param ch   字符 
  126.      */  
  127.     private static int getIndexOfCount(String text, char ch) {  
  128.         int count = 0;  
  129.         for (int i = 0; i < text.length(); i++) {  
  130.             count = (text.charAt(i) == ch) ? count + 1 : count;  
  131.         }  
  132.         return count;  
  133.     }  
  134. }  


  好了,下面给出PaginationInterceptor的完整代码: 
Java代码  收藏代码
  1. package com.ridge.dao.mybatis;  
  2.   
  3. import org.apache.commons.lang.reflect.FieldUtils;  
  4. import org.apache.commons.logging.Log;  
  5. import org.apache.commons.logging.LogFactory;  
  6. import org.apache.ibatis.executor.statement.PreparedStatementHandler;  
  7. import org.apache.ibatis.executor.statement.RoutingStatementHandler;  
  8. import org.apache.ibatis.executor.statement.StatementHandler;  
  9. import org.apache.ibatis.mapping.BoundSql;  
  10. import org.apache.ibatis.mapping.ParameterMapping;  
  11. import org.apache.ibatis.plugin.*;  
  12. import org.apache.ibatis.session.Configuration;  
  13. import org.apache.ibatis.session.RowBounds;  
  14. import org.hibernate.dialect.Dialect;  
  15.   
  16. import java.sql.Connection;  
  17. import java.util.Properties;  
  18. import java.util.regex.Matcher;  
  19. import java.util.regex.Pattern;  
  20.   
  21. /** 
  22.  * 默认情况下 
  23.  * 数据库的类型 
  24.  * 
  25.  * @author : chenxh 
  26.  * @date: 13-7-5 
  27.  */  
  28. @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})  
  29. public class PaginationInterceptor implements Interceptor {  
  30.   
  31.     private final static Log logger = LogFactory.getLog(PaginationInterceptor.class);  
  32.     public static final String CONFIGURATION = "configuration";  
  33.     private static Dialect dialect = null;  
  34.     private static final String ROW_BOUNDS = "rowBounds";  
  35.     private static final String BOUND_SQL = "boundSql";  
  36.     private static final String DIALECT = "dialect";  
  37.     private static final String SQL = "sql";  
  38.     private static final String OFFSET = "offset";  
  39.     private static final String LIMIT = "limit";  
  40.     public static final String DELEGATE = "delegate";  
  41.     private static final int CONNECTION_INDEX = 0;  
  42.   
  43.     private static final String INTERCEPTOR_CONF = "<plugins>\n" +  
  44.             "<plugin interceptor=\"" + PaginationInterceptor.class.getCanonicalName() + "\">\n" +  
  45.             "<property name=\"dialect\" value=\"" + DialetHelper.getSupportDatabaseTypes() + "\"/>\n" +  
  46.             "</plugin>\n" +  
  47.             "</plugins>";  
  48.   
  49.     @Override  
  50.     public Object intercept(Invocation invocation) throws Throwable {  
  51.         RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();  
  52.         PreparedStatementHandler preparedStatHandler =  
  53.                 (PreparedStatementHandler) FieldUtils.readField(statementHandler, DELEGATE, true);  
  54.         final Object[] queryArgs = invocation.getArgs();  
  55.         Connection connection = (Connection) queryArgs[CONNECTION_INDEX];  
  56.   
  57.         RowBounds rowBounds = (RowBounds) FieldUtils.readField(preparedStatHandler, ROW_BOUNDS, true);  
  58.         BoundSql boundSql = (BoundSql) FieldUtils.readField(preparedStatHandler, BOUND_SQL, true);  
  59.   
  60.         Configuration configuration = (Configuration) FieldUtils.readField(preparedStatHandler, CONFIGURATION, true);  
  61.   
  62.         //没有分页,直接返回原调用  
  63.         if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {  
  64.             return invocation.proceed();  
  65.         }  
  66.   
  67.         //有分页  
  68.         String originalSql = boundSql.getSql();  
  69.   
  70.         //1.获取总行数,将行数绑定到当前线程中  
  71.         String countSql = MybatisHepler.getCountSql(originalSql);  
  72.         CountHelper.getCount(countSql, preparedStatHandler, configuration, boundSql, connection);  
  73.   
  74.   
  75.         //2.获取分页的结果集  
  76.         //////////////////////////////////////////////////////////////////////////////////////////////  
  77.         String pagingSql = dialect.getLimitString(originalSql, rowBounds.getOffset(), rowBounds.getLimit());  
  78.         FieldUtils.writeField(boundSql, SQL, pagingSql, true);  
  79.   
  80.   
  81.         int size = 0;  
  82.         size = getPageParamNum(originalSql, pagingSql);  
  83.   
  84.         if (size == 1) {  
  85.             ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, LIMIT, Integer.class);  
  86.             boundSql.getParameterMappings().add(builder.build());  
  87.             boundSql.setAdditionalParameter(LIMIT, rowBounds.getLimit());  
  88.         }  
  89.         if (size == 2) {  
  90.   
  91.             ParameterMapping.Builder builder = new ParameterMapping.Builder(  
  92.                     configuration, OFFSET, Integer.class);  
  93.             boundSql.getParameterMappings().add(builder.build());  
  94.             boundSql.setAdditionalParameter(OFFSET, rowBounds.getOffset());  
  95.   
  96.             builder = new ParameterMapping.Builder(configuration, LIMIT,  
  97.                     Integer.class);  
  98.             boundSql.getParameterMappings().add(builder.build());  
  99.             boundSql.setAdditionalParameter(LIMIT, rowBounds.getLimit());  
  100.         }  
  101.         FieldUtils.writeField(rowBounds, OFFSET, RowBounds.NO_ROW_OFFSET, true);  
  102.         FieldUtils.writeField(rowBounds, LIMIT, RowBounds.NO_ROW_LIMIT, true);  
  103.   
  104.         if (logger.isDebugEnabled()) {  
  105.             logger.debug("\n" + originalSql +  
  106.                     "\n对应的分页SQL:\n" +  
  107.                     boundSql.getSql() +  
  108.                     "\n对应的count SQL:\n" +  
  109.                     countSql);  
  110.         }  
  111.         return invocation.proceed();  
  112.     }  
  113.   
  114.     /** 
  115.      * 获取用于控制分页的问号的个数 
  116.      * 
  117.      * @param originalSql 
  118.      * @param pagingSql 
  119.      * @return 
  120.      */  
  121.     private int getPageParamNum(String originalSql, String pagingSql) {  
  122.         int size = 0;  
  123.         String addSql = pagingSql.replace(originalSql, "");  
  124.   
  125.         Pattern pattern = Pattern.compile("[?]");  
  126.         Matcher matcher = pattern.matcher(addSql);  
  127.         while (matcher.find()) {  
  128.             size++;  
  129.         }  
  130.         return size;  
  131.     }  
  132.   
  133.     @Override  
  134.     public Object plugin(Object target) {  
  135.         return Plugin.wrap(target, this);  
  136.     }  
  137.   
  138.     @Override  
  139.     public void setProperties(Properties properties) {  
  140.         if (PaginationInterceptor.dialect == null) {  
  141.             String dialect = properties.getProperty(DIALECT);  
  142.             if (dialect == null) {  
  143.                 throw new RuntimeException("拦截器未提供dialect的配置,正确配置参见:\n" + INTERCEPTOR_CONF);  
  144.             }  
  145.             PaginationInterceptor.dialect = DialetHelper.getDialect(dialect);  
  146.         }  
  147.     }  
  148. }  


如何在保证分页接口签名不变的情况下,将总行数传回去呢? 

   下面是一个Service层的方法: 
Java代码  收藏代码
  1. public Page<App> queryAppList(AppQueryParam queryParam,Paging paging){  
  2.     List<App> apps = appMapper.queryList(queryParam, MybatisHepler.toRowBounds(paging));  
  3.     Page<App> appPage = new Page<App>();  
  4.     appPage.setResult(apps);  
  5.     appPage.setPageSize(paging.getPageSize());  
  6.     return appPage;  
  7. }  


由于DAO appMapper按正常签名只返回一个List,对应的总行数我怎么获取呢?这里我用到了ThreadLocal,因为它让我们可以跨类访问,毕竟Service调用DAO,它们都位于同一个Thread中: 
Java代码  收藏代码
  1. package com.ridge.dao.mybatis;  
  2.   
  3. import org.apache.commons.lang.reflect.FieldUtils;  
  4. import org.apache.ibatis.executor.statement.PreparedStatementHandler;  
  5. import org.apache.ibatis.mapping.BoundSql;  
  6. import org.apache.ibatis.mapping.MappedStatement;  
  7. import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;  
  8. import org.apache.ibatis.session.Configuration;  
  9. import org.slf4j.Logger;  
  10. import org.slf4j.LoggerFactory;  
  11.   
  12. import java.sql.Connection;  
  13. import java.sql.PreparedStatement;  
  14. import java.sql.ResultSet;  
  15.   
  16.   
  17. /** 
  18.  * @author : chenxh 
  19.  * @date: 13-7-8 
  20.  */  
  21. public class CountHelper {  
  22.   
  23.   
  24.     private static final String MAPPED_STATEMENT = "mappedStatement";  
  25.     private static Logger logger = LoggerFactory.getLogger(CountHelper.class);  
  26.   
  27.     /** 
  28.      * 保存计算总行数的值 
  29.      */  
  30.     private static ThreadLocal<Integer> totalRowCountHolder = new ThreadLocal<Integer>();  
  31.   
  32.     /** 
  33.      * 获取查询对象的总行数 
  34.      * @param sql  获取总行数的SQL 
  35.      * @param statementHandler 
  36.      * @param configuration 
  37.      * @param boundSql 
  38.      * @param connection 
  39.      * @throws Throwable 
  40.      */  
  41.     static void getCount(String sql, PreparedStatementHandler statementHandler,  
  42.                                 Configuration configuration,  BoundSql boundSql,  
  43.                                 Connection connection)  
  44.             throws Throwable{  
  45.         Object parameterObject = statementHandler.getParameterHandler().getParameterObject();  
  46.         if (logger.isDebugEnabled()) {  
  47.             logger.debug("Total count SQL [{}] ", sql);  
  48.             logger.debug("Total count Parameters: {} ", parameterObject);  
  49.         }  
  50.   
  51.   
  52.         PreparedStatement countStmt = null;  
  53.         ResultSet rs = null;  
  54.         try {  
  55.             countStmt = connection.prepareStatement(sql);  
  56.             final BoundSql countBS = new BoundSql(configuration, sql,  
  57.                     boundSql.getParameterMappings(), parameterObject);  
  58.   
  59.             MappedStatement mappedStatement = (MappedStatement)FieldUtils.readField(statementHandler, MAPPED_STATEMENT, true);  
  60.   
  61.             DefaultParameterHandler handler =  
  62.                     new DefaultParameterHandler(mappedStatement, parameterObject, countBS);  
  63.             handler.setParameters(countStmt);  
  64.   
  65.             rs = countStmt.executeQuery();  
  66.             int count = 0;  
  67.             if (rs.next()) {  
  68.                 count = rs.getInt(1);  
  69.             }  
  70.             if (logger.isDebugEnabled()) {  
  71.                 logger.debug("Total count: {}", count);  
  72.             }  
  73.             totalRowCountHolder.set(count);  
  74.         } finally {  
  75.             try {  
  76.                 if (rs != null) {  
  77.                     rs.close();  
  78.                 }  
  79.             } finally {  
  80.                 if (countStmt != null) {  
  81.                     countStmt.close();  
  82.                 }  
  83.             }  
  84.         }  
  85.     }  
  86.   
  87.     /** 
  88.      * 获取当前线程对应的分页查询的总行数 
  89.      * 
  90.      * @return 
  91.      */  
  92.     public static int getTotalRowCount() {  
  93.         return totalRowCountHolder.get();  
  94.     }  
  95. }  



这样,以上Service的方法就可以改成: 
Java代码  收藏代码
  1. public Page<App> queryAppList(AppQueryParam queryParam,Paging paging){  
  2.     List<App> apps = appMapper.queryList(queryParam, MybatisHepler.toRowBounds(paging));  
  3.     Page<App> appPage = new Page<App>();  
  4.     appPage.setResult(apps);  
  5.     appPage.setPageSize(paging.getPageSize());  
  6.     appPage.setTotalRows(CountHelper.getTotalRowCount());//①注意这里!!  
  7.     return appPage;  
  8. }  


改进 
  
   但是每个Service都要手工调用setTotalRows(CountHelper.getTotalRowCount())是不是有点多余呢? 
   
   这里我们可以使用Spring AOP自动做这个事,这样①处的代码就可以不用手工写了。 
为些,我写了一个Advice: 
Java代码  收藏代码
  1. package com.ridge.dao.mybatis;  
  2.   
  3. import com.ridge.dao.Page;  
  4.   
  5. /** 
  6.  * @author : chenxh 
  7.  * @date: 13-7-8 
  8.  */  
  9. public class TotalRowValueMount {  
  10.   
  11.     public void setTotalRows(Page page){  
  12.        page.setTotalRows(CountHelper.getTotalRowCount());  
  13.     }  
  14. }  


   在Spring配置文件中,将TotalRowValueMount#setTotalRows(Page page)方法植入到所有返回值类型为Page的方法上,在方法返回时,自动调用setTotalRows(CountHelper.getTotalRowCount()); 

Xml代码  收藏代码
  1. <!-- 所有分页查询的方法自动设置总行数 -->  
  2. <aop:config>  
  3.     <aop:aspect id="pagingQueryAspect" ref="totalRowCounter">  
  4.        <aop:pointcut id="pagingQueryMethods" expression="execution(public com.ridge.dao.Page *(..))"/>  
  5.         <aop:after-returning  pointcut-ref="pagingQueryMethods"  returning="page"  method="setTotalRows"/>  
  6.     </aop:aspect>  
  7. </aop:config>  
  8. <bean id="totalRowCounter" class="com.ridge.dao.mybatis.TotalRowValueMount"/>  


怎么配置mybatis以应用拦截器呢 

  在mybatis配置文件中,添加拦截器即可: 
Xml代码  收藏代码
  1.    <?xml version="1.0" encoding="UTF-8"?>  
  2. <!DOCTYPE configuration  
  3.         PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
  4.         "http://mybatis.org/dtd/mybatis-3-config.dtd">  
  5.   
  6. <configuration>  
  7.     <settings>  
  8.         <!-- changes from the defaults -->  
  9.         <setting name="lazyLoadingEnabled" value="false" />  
  10.     </settings>  
  11.     <plugins>  
  12.         <plugin interceptor="com.ridge.dao.mybatis.PaginationInterceptor">  
  13.             <property name="dialect" value="MYSQL"/>  
  14.         </plugin>  
  15.     </plugins>  
  16.     <mappers>  
  17.         <package name="com.xxx.yyy"/>  
  18.     </mappers>  
  19. </configuration>  

   
0 0
原创粉丝点击