MyBatis 通用分页

引用:http://xdwangiflytek.iteye.com/blog/1750641

分页分为真分页和假分页,而MyBatis本身没有提供基于数据库方言的分页功能,而是基于JDBC的游标分页,很容易出现性能问题。网上提供的一个解决方案感觉还不错,是基于MyBatis本身的插件机制,通过拦截Sql做分页。

         首先,我们需要根据不同数据库来加载不同的分页SQL,这里我们参考Hibernate,定义一个数据库方言接口

Dialect.java

Java代码   收藏代码
  1. package com.iflytek.mybatis.page.dialect;  
  2.   
  3. /** 
  4.  * @author xdwang 
  5.  *  
  6.  * @ceate 2012-12-19 下午7:45:24 
  7.  *  
  8.  * @description 数据库方言接口 
  9.  *  
  10.  */  
  11.   
  12. public interface Dialect {  
  13.   
  14.     public static enum Type {  
  15.         MYSQL {  
  16.             public String getValue() {  
  17.                 return "mysql";  
  18.             }  
  19.         },  
  20.         MSSQL {  
  21.             public String getValue() {  
  22.                 return "sqlserver";  
  23.             }  
  24.         },  
  25.         ORACLE {  
  26.             public String getValue() {  
  27.                 return "oracle";  
  28.             }  
  29.         }  
  30.     }  
  31.   
  32.     /** 
  33.      * @descrption 获取分页SQL 
  34.      * @author xdwang 
  35.      * @create 2012-12-19下午7:48:44 
  36.      * @param sql 
  37.      *            原始查询SQL 
  38.      * @param offset 
  39.      *            开始记录索引(从零开始) 
  40.      * @param limit 
  41.      *            每页记录大小 
  42.      * @return 返回数据库相关的分页SQL语句 
  43.      */  
  44.     public abstract String getPageSql(String sql, int offset, int limit);  
  45.   
  46. }  

 然后分别定义不同类型数据库的具体分页,这里我们列举3个比较常用的,MySQL、MSSQL、Oracle
MySql5Dialect.java

Java代码   收藏代码
  1. package com.iflytek.mybatis.page.dialect;  
  2.   
  3. /** 
  4.  * @author xdwang 
  5.  *  
  6.  * @ceate 2012-12-19 下午7:50:44 
  7.  *  
  8.  * @description MySQL数据库实现 
  9.  *  
  10.  */  
  11. public class MySql5Dialect implements Dialect {  
  12.   
  13.     protected static final String SQL_END_DELIMITER = ";";  
  14.   
  15.     public String getPageSql(String sql, boolean hasOffset) {  
  16.         return MySql5PageHepler.getPageSql(sql, -1, -1);  
  17.     }  
  18.   
  19.     public String getPageSql(String sql, int offset, int limit) {  
  20.         return MySql5PageHepler.getPageSql(sql, offset, limit);  
  21.     }  
  22.   
  23.     public boolean supportsLimit() {  
  24.         return true;  
  25.     }  
  26. }  

 MySql5PageHepler.java

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

 

 OracleDialect.java

Java代码   收藏代码
  1. package com.iflytek.mybatis.page.dialect;  
  2.   
  3. /** 
  4.  * @author xdwang 
  5.  *  
  6.  * @ceate 2012-12-19 下午7:54:56 
  7.  *  
  8.  * @description Oracle数据库实现 
  9.  *  
  10.  */  
  11. public class OracleDialect implements Dialect {  
  12.   
  13.     public String getPageSql(String sql, int offset, int limit) {  
  14.         sql = sql.trim();  
  15.         boolean isForUpdate = false;  
  16.         if (sql.toLowerCase().endsWith(" for update")) {  
  17.             sql = sql.substring(0, sql.length() - 11);  
  18.             isForUpdate = true;  
  19.         }  
  20.   
  21.         StringBuffer pageSql = new StringBuffer(sql.length() + 100);  
  22.         pageSql.append("select * from ( select row_.*, rownum rownum_ from ( ");  
  23.         pageSql.append(sql);  
  24.         pageSql.append(" ) row_ ) where rownum_ > " + offset + " and rownum_ <= " + (offset + limit));  
  25.         if (isForUpdate) {  
  26.             pageSql.append(" for update");  
  27.         }  
  28.         return pageSql.toString();  
  29.     }  
  30.   
  31. }  
 

SQLServerDialect.java

Java代码   收藏代码
  1. package com.iflytek.mybatis.page.dialect;  
  2.   
  3. /** 
  4.  * @author xdwang 
  5.  *  
  6.  * @ceate 2012-12-19 下午7:53:14 
  7.  *  
  8.  * @description SQLServer数据库实现 
  9.  *  
  10.  */  
  11. public class SQLServerDialect implements Dialect {  
  12.   
  13.     public String getPageSql(String sql, int offset, int limit) {  
  14.         sql = sql.trim();  
  15.         StringBuffer pageSql = new StringBuffer(sql.length() + 100);  
  16.         // 其实这里还是有一点问题的,就是排序问题,指定死了,有解决的提供一下,等复习到Hibernate看看Hibernat内部是如何实现的。  
  17.         pageSql.append("select * from(select a.*,row_number() over (order by id desc) rownum from( ");  
  18.         pageSql.append(sql);  
  19.         pageSql.append(") a )b where rownum> " + offset + " and rownum <= " + (offset + limit));  
  20.         return pageSql.toString();  
  21.     }  
  22.   
  23. }  
 

然后我们定义拦截器
PaginationInterceptor.java

Java代码   收藏代码
  1. package com.iflytek.mybatis.page.interceptor;  
  2.   
  3. import java.sql.Connection;  
  4. import java.util.Properties;  
  5.   
  6. import org.apache.commons.logging.Log;  
  7. import org.apache.commons.logging.LogFactory;  
  8. import org.apache.ibatis.executor.statement.StatementHandler;  
  9. import org.apache.ibatis.mapping.BoundSql;  
  10. import org.apache.ibatis.plugin.Interceptor;  
  11. import org.apache.ibatis.plugin.Intercepts;  
  12. import org.apache.ibatis.plugin.Invocation;  
  13. import org.apache.ibatis.plugin.Plugin;  
  14. import org.apache.ibatis.plugin.Signature;  
  15. import org.apache.ibatis.reflection.MetaObject;  
  16. import org.apache.ibatis.session.Configuration;  
  17. import org.apache.ibatis.session.RowBounds;  
  18.   
  19. import com.iflytek.mybatis.page.dialect.Dialect;  
  20. import com.iflytek.mybatis.page.dialect.MySql5Dialect;  
  21. import com.iflytek.mybatis.page.dialect.OracleDialect;  
  22.   
  23. /** 
  24.  *  
  25.  * @author xdwang 
  26.  *  
  27.  * @ceate 2012-12-19 下午8:01:31 
  28.  *  
  29.  * @description 然后就是实现mybatis提供的拦截器接口,编写我们自己的分页实现,原理就是拦截底层JDBC操作相关的Statement对象, 
  30.  *              把前端的分页参数如当前记录索引和每页大小通过拦截器注入到sql语句中 
  31.  *              ,即在sql执行之前通过分页参数重新生成分页sql,而具体的分页sql实现是分离到Dialect接口中去。 
  32.  *  
  33.  *  
  34.  */  
  35. @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })  
  36. public class PaginationInterceptor implements Interceptor {  
  37.   
  38.     private final static Log log = LogFactory.getLog(PaginationInterceptor.class);  
  39.   
  40.     public Object intercept(Invocation invocation) throws Throwable {  
  41.         StatementHandler statementHandler = (StatementHandler) invocation.getTarget();  
  42.         BoundSql boundSql = statementHandler.getBoundSql();  
  43.         MetaObject metaStatementHandler = MetaObject.forObject(statementHandler);  
  44.         RowBounds rowBounds = (RowBounds) metaStatementHandler.getValue("delegate.rowBounds");  
  45.         if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {  
  46.             return invocation.proceed();  
  47.         }  
  48.         Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");  
  49.         Dialect.Type databaseType = null;  
  50.         try {  
  51.             databaseType = Dialect.Type.valueOf(configuration.getVariables().getProperty("dialect").toUpperCase());  
  52.         } catch (Exception e) {  
  53.             // ignore  
  54.         }  
  55.         if (databaseType == null) {  
  56.             throw new RuntimeException("the value of the dialect property in configuration.xml is not defined : " + configuration.getVariables().getProperty("dialect"));  
  57.         }  
  58.         Dialect dialect = null;  
  59.         switch (databaseType) {  
  60.         case MYSQL:  
  61.             dialect = new MySql5Dialect();  
  62.             break;  
  63.         case MSSQL:  
  64.             dialect = new MySql5Dialect();  
  65.             break;  
  66.         case ORACLE:  
  67.             dialect = new OracleDialect();  
  68.             break;  
  69.         default:  
  70.             dialect = new MySql5Dialect();  
  71.         }  
  72.   
  73.         String originalSql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");  
  74.         metaStatementHandler.setValue("delegate.boundSql.sql", dialect.getPageSql(originalSql, rowBounds.getOffset(), rowBounds.getLimit()));  
  75.         metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);  
  76.         metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);  
  77.         if (log.isDebugEnabled()) {  
  78.             log.debug("生成分页SQL : " + boundSql.getSql());  
  79.         }  
  80.         return invocation.proceed();  
  81.     }  
  82.   
  83.     public Object plugin(Object target) {  
  84.         return Plugin.wrap(target, this);  
  85.     }  
  86.   
  87.     public void setProperties(Properties properties) {  
  88.     }  
  89.   
  90. }  
 

Ok,搞定了,下面看看如何使用,其实和直接调用MyBatis原生的假分页方式一样。只需要在mybatis-config.xml添加一个标识和一个插件

Xml代码   收藏代码
  1. <properties>  
  2.     <property name="dialect" value="mysql" />  
  3. </properties>  
  4.   
  5. <plugins>  
  6.     <plugin interceptor="com.iflytek.mybatis.page.interceptor.PaginationInterceptor">  
  7.     </plugin>  
  8. </plugins>  

 然后和MyBatis默认提供分页的方式一样,直接调用

Java代码   收藏代码
  1. public List<Student> getStudentsByPage(){  
  2.     List<Student> students = new ArrayList<Student>();  
  3.     SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();  
  4.     try {  
  5.         //从第一条开始,取4条记录  
  6.         RowBounds rowBounds = new RowBounds(1,4);  
  7.         Student student=new Student();  
  8.         student.setName("xdwang");  
  9.         students = sqlSession.selectList("com.iflytek.dao.mapper.StudentMapper.selectByPageList", student, rowBounds);  
  10.         sqlSession.commit();  
  11.     } finally {  
  12.         sqlSession.close();  
  13.     }  
  14.     return students;  
  15.       
  16. }  

 

Ok,搞定,当然,上面我们也可以将需要拦截添加的Sql写在mapper.xml中,然后再需要分页的查询语句中引用,只是需要在每个模块下分页的地方都引用,相对来说比较麻烦点(其实也还好)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值