博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
MyBatis:简单物理分页实现(Plugin)
阅读量:6113 次
发布时间:2019-06-21

本文共 7802 字,大约阅读时间需要 26 分钟。

  hot3.png

一、必要性

首先,介绍一下使用自定义拦截器来进行物理分页的必要性。我们知道MyBatis中的SqlSession接口中提供一个带分页功能的方法:

public interface SqlSession extends Closeable {    
 List
 selectList(String statement, Object parameter, RowBounds rowBounds);    // ....}

使用该方法,我们在查询时可以通过为selectList(..)方法提供一个RowBounds参数,来使该语句带有分页功能,举个例如,假设我需要取出查询记录的前三条记录,可以这样:

// 获取sqlSession的步骤略,statement略,mapper中的映射语句为 select * from usersList
 list = sqlSession.selectList(statement, null, new RowBounds(0,3));

这时我们获取到的记录就是查询记录的前三条记录(select * from users的查询结果)

这时我们会有个疑问,既然MyBatis已经为我们提供了分页的处理类,为何我们还要再重复造轮子(再手动写一个拦截器)呢?

这是因为MyBatis内置的分页处理器,是通过内存进行分页,结合上面的例子就是MyBatis首先执行select * from users,然后获取结果集ResultSet,接着通过传入的RowBounds中的offset和limit属性来对ResultSet进行加工。如果记录量大的话,这种效率无疑是相当低的。想证实上面这个结论,可以查看MyBatis中的DefaultResultSetHandler类

public class DefaultResultSetHandler implements ResultSetHandler {    // ....        private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,             ResultHandler
 resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {                DefaultResultContext resultContext = new DefaultResultContext();        // 通过skipRows来使ResusltSet指向rowBounds中的offset所指定的位置        skipRows(rsw.getResultSet(), rowBounds);                while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {            ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);            Object rowValue = getRowValue(rsw, discriminatedResultMap);            storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());        }    }    private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {        // 如果ResultSet中的光标支持前后移动        if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {            // 如果rowBounds中的offset值不为0            if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {                // 将rs的光标移至rowBounds中的offset指定的位置                rs.absolute(rowBounds.getOffset());            }        } else {    // 如果ResultSet只支持向前移动            // 将光标从0的位置使用rs.next()来每次向前移动一位,直至rowBounds中offset指定的位置            for (int i = 0; i < rowBounds.getOffset(); i++) {                rs.next();            }        }    }        // ....}

欲证实可在这个类中的handleRowValuesForSimpleResultMap(...)中的skipRows(...)方法上打个断点,在skipRows(...)中的rs.absolute(...)和rs.next()分别打上断点,然后通过debug方式执行

List
 list = sqlSession.selectList(statement, null, new RowBounds(0,3));

即可看到程序确实进入了skipRows(...)方法中,我们可以看到在skipRows(...)中,MyBatis是通过执行select * from users再对ResultSet结果集进行加工处理,而不是直接执行select * from users limit 0,3(假设是MySql数据库),这样效率显然是极低的,所以我们如果在实际应用中,有两种方式来解决这个问题

1)在mapper映射文件中手动将每次执行的语句改为select * from users limit #{offset},#{limit}

2)自定义一个拦截器,来将底层的最终查询语句变更为select * from users limit 0,3

下面将介绍第2种解决方式。

二、自定义分页插件

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

// 前面是允许用插件拦截的类名,括号里是允许用插件拦截的方法名Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)ParameterHandler (getParameterObject, setParameters)ResultSetHandler (handleResultSets, handleOutputParameters)StatementHandler (prepare, parameterize, batch, update, query)

MyBatis是在StatementHandler中的prepare(...)方法中完成对sql的解析,所以我们需要在这个方法前设置一个拦截器也就是plugin来进行sql语句的置换,下面是具体的代码:

package cn.kolbe.mybatis.plugin;import java.lang.reflect.Field;import java.sql.Connection;import java.util.Properties;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.session.RowBounds;@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }))public class PagePlugin implements Interceptor {	// select语句正则表达式匹配: ^代表开头位置 \s代表空格 $代表结尾位置 *代表任意多个 .代表任意字符	private final static String REGEX = "^\\s*[Ss][Ee][Ll][Ee][Cc][Tt].*$";			@Override	public Object intercept(Invocation inv) throws Throwable {		// 此时的target为RoutingStatementHandler类的实例		StatementHandler target = (StatementHandler)inv.getTarget();		// BoundSql类中有一个sql属性,即为待执行的sql语句		BoundSql boundSql = target.getBoundSql();		String sql = boundSql.getSql();		// 如果sql语句是select语句的话,则进行查看是否需要分页处理		if (sql.matches(REGEX)) {			// delegate是RoutingStatementHandler通过mapper映射文件中设置的statementType来指定具体的StatementHandler			Object delegate = readField(target, "delegate");			// rowBounds中绑定了我们自定义的分页信息,包括起始位置offset和取出记录条数limit			RowBounds rowBounds = (RowBounds)readField(delegate, "rowBounds");			// 如果rowBound不为空,且rowBounds的起始位置不为0,则代表我们需要进行分页处理			if (rowBounds != null && rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {				// assemSql(...)完成对sql语句的装配及rowBounds的重置操作				writeField(boundSql, "sql", assemSql(sql, rowBounds));			}		}		return inv.proceed();	}	/**	 * 装配SQL语句,并重置RowBounds中的offset和limit	 * @param oldSql	 * @param rowBounds	 * @return	 */	public String assemSql(String oldSql, RowBounds rowBounds) throws Exception {		String sql = oldSql + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();		// 这两步是必须的,因为在前面置换好sql语句以后,实际的结果集就是我们想要的所以offset和limit必须重置为初始值		writeField(rowBounds, "offset", RowBounds.NO_ROW_OFFSET);		writeField(rowBounds, "limit", RowBounds.NO_ROW_LIMIT);		return sql;	}			/**	 * 利用反射获取指定对象的指定属性	 * @param target	 * @param fieldName	 * @return	 * @throws Exception	 */	private Object readField(Object target, String fieldName) throws Exception {		Field field = null;		// 遍历target的属性及其父类的属性		for (Class
 c = target.getClass(); c != null; c = c.getSuperclass()) { try { field = c.getDeclaredField(fieldName); } catch (NoSuchFieldException ex) {                                // 没找到该属性,则继承查找父类的属性,所以不处理该异常 } } field.setAccessible(true); return field.get(target); } /**  * 利用反射为指定对象的指定属性写入值  * @param target  * @param fieldName  * @param value  * @throws Exception  */ private void writeField(Object target, String fieldName, Object value) throws Exception { Field field = null; // 遍历target的属性及其父类的属性 for (Class
 c = target.getClass(); c != null; c = c.getSuperclass()) { try { field = c.getDeclaredField(fieldName); } catch (NoSuchFieldException ex) {                                // 没找到该属性,则继承查找父类的属性,所以不处理该异常 } } field.setAccessible(true); field.set(target, value); } @Override public Object plugin(Object target) { // 通过Plugin的wrap(...)方法来实现代理类的生成操作 return Plugin.wrap(target, this); } @Override public void setProperties(Properties props) {} }

注:

1)代码中为了尽量保持简单易懂,没有使用过多的工具集,具体应用中对对象的私有属性赋值获取和赋值操作可以通过MyBatis内置的类或apache的commons-lang工具来处理

2)该例子使用MySql作为示例,没有考虑其它数据库,具体应用中可以考虑通过配置文件中来设置数据库,并动态的根据配置文件来决定sql语句的具体装配,同样为了简单性,在此就不举例了

在MyBatis的配置文件mybatis-config.xml中配置该插件

    

在映射文件中添加查询语句

select * from users

在应用中使用

package cn.kolbe.mybatis;import java.io.FileInputStream;import java.io.InputStream;import java.util.List;import org.apache.ibatis.session.RowBounds;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import org.junit.Test;import cn.kolbe.mybatis.domain.User;public class MyBatisTest {	@Test	public void queryByPage() throws Exception {		InputStream in = new FileInputStream("src/main/java/mybatis-config.xml");		SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);		SqlSession session = factory.openSession();		String statement = "cn.kolbe.mybatis.domain.UserMapper.getAll";		List
 list = session.selectList(statement, null, new RowBounds(0,3)); System.out.println(list); } }

转载于:https://my.oschina.net/kolbe/blog/513382

你可能感兴趣的文章
SqlServer2008第一次安装后连接问题
查看>>
cocos2d-x Schedule详解
查看>>
sdut 2163:Identifiers(第二届山东省省赛原题,水题)
查看>>
C++ 容器:顺序性容器、关联式容器和容器适配器
查看>>
mysql 常用语句集
查看>>
Atitit.软件开发提升稳定性总结
查看>>
lftp查看文件时间与登录服务查看文件时间相差8小时
查看>>
[leetcode]Next Permutation @ Python
查看>>
JAVA(2)——JDBC
查看>>
php heredoc 与 nowdoc
查看>>
DBA_Oracle DBA常用表汇总(概念)
查看>>
第30周二
查看>>
数学类杂志SCI2013-2014影响因子
查看>>
实用的树形菜单控件tree
查看>>
最近公共祖先(lca)
查看>>
【WP 8.1开发】文件选取器的使用方法
查看>>
Java实现BASE64编解码
查看>>
【Java】java基本知识
查看>>
之前学习wordpress的几张图片
查看>>
RT-Thread下的串口驱动程序分析【转载】
查看>>