设为首页 收藏本站
查看: 1075|回复: 0

[经验分享] MyBatis 通用分页

[复制链接]

尚未签到

发表于 2016-11-24 09:05:36 | 显示全部楼层 |阅读模式
分页分为真分页和假分页,而
MyBatis
本身没有提供基于数据库方言的分页功能,而是基于
JDBC
的游标分页,很容易出现性能问题。网上提供的一个解决方案感觉还不错,是基于
MyBatis
本身的插件机制,通过拦截
Sql
做分页。

        

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

Dialect.java


package com.iflytek.mybatis.page.dialect;
/**
* @author xdwang
*
* @ceate 2012-12-19 下午7:45:24
*
* @description 数据库方言接口
*
*/
public interface Dialect {
public static enum Type {
MYSQL {
public String getValue() {
return "mysql";
}
},
MSSQL {
public String getValue() {
return "sqlserver";
}
},
ORACLE {
public String getValue() {
return "oracle";
}
}
}
/**
* @descrption 获取分页SQL
* @author xdwang
* @create 2012-12-19下午7:48:44
* @param sql
*            原始查询SQL
* @param offset
*            开始记录索引(从零开始)
* @param limit
*            每页记录大小
* @return 返回数据库相关的分页SQL语句
*/
public abstract String getPageSql(String sql, int offset, int limit);
}

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

MySql5Dialect.java

package com.iflytek.mybatis.page.dialect;
/**
* @author xdwang
*
* @ceate 2012-12-19 下午7:50:44
*
* @description MySQL数据库实现
*
*/
public class MySql5Dialect implements Dialect {
protected static final String SQL_END_DELIMITER = ";";
public String getPageSql(String sql, boolean hasOffset) {
return MySql5PageHepler.getPageSql(sql, -1, -1);
}
public String getPageSql(String sql, int offset, int limit) {
return MySql5PageHepler.getPageSql(sql, offset, limit);
}
public boolean supportsLimit() {
return true;
}
}

  MySql5PageHepler.java

package com.iflytek.mybatis.page.dialect;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author xdwang
*
* @ceate 2012-12-19 下午8:41:21
*
* @description MySql辅助方法
*
*/
public class MySql5PageHepler {
/**
* @descrption 得到查询总数的sql
* @author xdwang
* @create 2012-12-19下午8:41:10
* @param querySelect
* @return
*/
public static String getCountString(String querySelect) {
querySelect = getLineSql(querySelect);
int orderIndex = getLastOrderInsertPoint(querySelect);
int formIndex = getAfterFormInsertPoint(querySelect);
String select = querySelect.substring(0, formIndex);
// 如果SELECT 中包含 DISTINCT 只能在外层包含COUNT
if (select.toLowerCase().indexOf("select distinct") != -1 || querySelect.toLowerCase().indexOf("group by") != -1) {
return new StringBuffer(querySelect.length()).append("select count(1) count from (").append(querySelect.substring(0, orderIndex)).append(" ) t").toString();
} else {
return new StringBuffer(querySelect.length()).append("select count(1) count ").append(querySelect.substring(formIndex, orderIndex)).toString();
}
}
/**
* 得到最后一个Order By的插入点位置
*
* @return 返回最后一个Order By插入点的位置
*/
private static int getLastOrderInsertPoint(String querySelect) {
int orderIndex = querySelect.toLowerCase().lastIndexOf("order by");
if (orderIndex == -1 || !isBracketCanPartnership(querySelect.substring(orderIndex, querySelect.length()))) {
throw new RuntimeException("My SQL 分页必须要有Order by 语句!");
}
return orderIndex;
}
/**
* 得到分页的SQL
*
* @param offset
*            偏移量
* @param limit
*            位置
* @return 分页SQL
*/
public static String getPageSql(String querySelect, int offset, int limit) {
querySelect = getLineSql(querySelect);
String sql = querySelect.replaceAll("[^\\s,]+\\.", "") + " limit " + offset + " ," + limit;
return sql;
}
/**
* 将SQL语句变成一条语句,并且每个单词的间隔都是1个空格
*
* @param sql
*            SQL语句
* @return 如果sql是NULL返回空,否则返回转化后的SQL
*/
private static String getLineSql(String sql) {
return sql.replaceAll("[\r\n]", " ").replaceAll("\\s{2,}", " ");
}
/**
* 得到SQL第一个正确的FROM的的插入点
*/
private static int getAfterFormInsertPoint(String querySelect) {
String regex = "\\s+FROM\\s+";
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(querySelect);
while (matcher.find()) {
int fromStartIndex = matcher.start(0);
String text = querySelect.substring(0, fromStartIndex);
if (isBracketCanPartnership(text)) {
return fromStartIndex;
}
}
return 0;
}
/**
* 判断括号"()"是否匹配,并不会判断排列顺序是否正确
*
* @param text
*            要判断的文本
* @return 如果匹配返回TRUE,否则返回FALSE
*/
private static boolean isBracketCanPartnership(String text) {
if (text == null || (getIndexOfCount(text, '(') != getIndexOfCount(text, ')'))) {
return false;
}
return true;
}
/**
* 得到一个字符在另一个字符串中出现的次数
*
* @param text
*            文本
* @param ch
*            字符
*/
private static int getIndexOfCount(String text, char ch) {
int count = 0;
for (int i = 0; i < text.length(); i++) {
count = (text.charAt(i) == ch) ? count + 1 : count;
}
return count;
}
}

  OracleDialect.java

package com.iflytek.mybatis.page.dialect;
/**
* @author xdwang
*
* @ceate 2012-12-19 下午7:54:56
*
* @description Oracle数据库实现
*
*/
public class OracleDialect implements Dialect {
public String getPageSql(String sql, int offset, int limit) {
sql = sql.trim();
boolean isForUpdate = false;
if (sql.toLowerCase().endsWith(" for update")) {
sql = sql.substring(0, sql.length() - 11);
isForUpdate = true;
}
StringBuffer pageSql = new StringBuffer(sql.length() + 100);
pageSql.append("select * from ( select row_.*, rownum rownum_ from ( ");
pageSql.append(sql);
pageSql.append(" ) row_ ) where rownum_ > " + offset + " and rownum_ <= " + (offset + limit));
if (isForUpdate) {
pageSql.append(" for update");
}
return pageSql.toString();
}
}

   SQLServerDialect.java

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

   然后我们定义拦截器

PaginationInterceptor.java

package com.iflytek.mybatis.page.interceptor;
import java.sql.Connection;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;
import com.iflytek.mybatis.page.dialect.Dialect;
import com.iflytek.mybatis.page.dialect.MySql5Dialect;
import com.iflytek.mybatis.page.dialect.OracleDialect;
/**
*
* @author xdwang
*
* @ceate 2012-12-19 下午8:01:31
*
* @description 然后就是实现mybatis提供的拦截器接口,编写我们自己的分页实现,原理就是拦截底层JDBC操作相关的Statement对象,
*              把前端的分页参数如当前记录索引和每页大小通过拦截器注入到sql语句中
*              ,即在sql执行之前通过分页参数重新生成分页sql,而具体的分页sql实现是分离到Dialect接口中去。
*
*
*/
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class PaginationInterceptor implements Interceptor {
private final static Log log = LogFactory.getLog(PaginationInterceptor.class);
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler);
RowBounds rowBounds = (RowBounds) metaStatementHandler.getValue("delegate.rowBounds");
if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {
return invocation.proceed();
}
Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");
Dialect.Type databaseType = null;
try {
databaseType = Dialect.Type.valueOf(configuration.getVariables().getProperty("dialect").toUpperCase());
} catch (Exception e) {
// ignore
}
if (databaseType == null) {
throw new RuntimeException("the value of the dialect property in configuration.xml is not defined : " + configuration.getVariables().getProperty("dialect"));
}
Dialect dialect = null;
switch (databaseType) {
case MYSQL:
dialect = new MySql5Dialect();
break;
case MSSQL:
dialect = new MySql5Dialect();
break;
case ORACLE:
dialect = new OracleDialect();
break;
default:
dialect = new MySql5Dialect();
}
String originalSql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
metaStatementHandler.setValue("delegate.boundSql.sql", dialect.getPageSql(originalSql, rowBounds.getOffset(), rowBounds.getLimit()));
metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
if (log.isDebugEnabled()) {
log.debug("生成分页SQL : " + boundSql.getSql());
}
return invocation.proceed();
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
}
}

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

<properties>
<property name="dialect" value="mysql" />
</properties>
<plugins>
<plugin interceptor="com.iflytek.mybatis.page.interceptor.PaginationInterceptor">
</plugin>
</plugins>

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

public List<Student> getStudentsByPage(){
List<Student> students = new ArrayList<Student>();
SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
try {
//从第一条开始,取4条记录
RowBounds rowBounds = new RowBounds(1,4);
Student student=new Student();
student.setName("xdwang");
students = sqlSession.selectList("com.iflytek.dao.mapper.StudentMapper.selectByPageList", student, rowBounds);
sqlSession.commit();
} finally {
sqlSession.close();
}
return students;
}

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

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-304781-1-1.html 上篇帖子: mybatis 改进之处 下篇帖子: Mybatis 动态SQL
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表