MyBatis:简单物理分页实现

一、必要性

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

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

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

// 获取sqlSession的步骤略,statement略,mapper中的映射语句为 select * from users
List<User> 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<Object> resultContext = new DefaultResultContext<Object>();
        // 通过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<User> 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中配置该插件

<plugins>
    <plugin interceptor="cn.kolbe.mybatis.plugin.PagePlugin"></plugin>
</plugins>

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

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/mybatis-3-mapper.dtd">
<mapper namespace="cn.kolbe.mybatis.domain.UserMapper">
	<select id="getAll" resultType="User">
		select * from users
	</select>
</mapper>

在应用中使用

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<User> list = session.selectList(statement, null, new RowBounds(0,3));
		System.out.println(list);
	} 
}
时间: 2024-08-05 18:14:45

MyBatis:简单物理分页实现的相关文章

Mybatis简单的入门之增删改查

一般的步骤如下 1.添加Mybatis所需要的包,和连接数据库所需的包 2.配置mybatis-config.xml文件 3.配置与pojo对应的映射文件 mybatis-config,xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http:/

mybatis简单sql使用java注解而不是xml配置

一直没有系统的接触mybatis,这就导致对其构建模式并没有清晰的认知,所以项目中所有的查询语句都使用在xml中配置,无论是简单sql,还是复杂sql,无一例外的没有使用java注解,这一点,现在看来,真是后悔莫及!那么请你牢记这点原则吧:mybatis简单sql使用java注解而不是xml配置! 再次使用mybatis,觉得有必要重新认识一下它.这就好比,在你上班的路上,如果偶尔抬抬头扫一扫你的周围,也许就会瞥见不一样的风景──非常有气质的美女映入眼帘,你不得不聚精会神的把眼光的焦点全部集中于

mybatis的物理分页:mybatis-paginator

github上有一个专门针对mybatis的物理分页开源项目:mybatis-paginator,兼容目前绝大多数主流数据库,十分好用,下面是使用步骤: 环境:struts2 + spring + mybatis 一.pom.xml中添加依赖项 1 <dependency> 2 <groupId>com.github.miemiedev</groupId> 3 <artifactId>mybatis-paginator</artifactId>

Mybatis 数据库物理分页插件 PageHelper

以前使用ibatis/mybatis,都是自己手写sql语句进行物理分页,虽然稍微有点麻烦,但是都习惯了.最近试用了下mybatis的分页插件 PageHelper,感觉还不错吧.记录下其使用方法. 1. 引入依赖jar包: <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>3.7.5&l

mybatis快速入门,mybatis简单实例, 如何使用mybatis

目录结构: 1. 导入所需要的包 2. 创建数据库 create database mybatis; use mybatis; CREATE TABLE users(id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(20), age INT); INSERT INTO users(NAME, age) VALUES('Tom', 12); INSERT INTO users(NAME, age) VALUES('Jack', 11); 3. 建立相应

MyBatis简单使用和入门理解

本文记录第一次使用Mybatis时碰到的一些错误和简单理解,采用的示例是Eclipse中的JAVA工程,采用XML文件定义数据库连接. 可以使用Java JDBC API直接操作数据库,但使用框架会更便捷.高效而且还可以利用框架提供的某些强大的功能(比如事务管理),而Mybatis就是这样的一个框架. Mybatis主要由四大部分组成: ①SqlSessionFactoryBuilder ②SqlSessionFactory ③SqlSession ④SQL Mapper 要想访问(操作)数据库

mybatis 简单使用示例(单独使用):

mybatis的单独使用简单示例: 步骤1: 新建xml文件. 示例: <?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace=

mybatis 简单入门

<pre class="html" name="code">1.1 创建测试项目,普通java项目或者是JavaWeb项目均可 <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUUAAAIhCAYAAADZ+aiMAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAF9TSURBVHhe7

myBatis简单入门

本文主要介绍一个现在比较流行的数据库操作框架Mybatis,只是简单的介绍入门用法 全部代码下载: Github链接:链接 写文章不易,欢迎大家采我的文章,以及给出有用的评论,当然大家也可以关注一下我的github:多谢: 1.MyBatis 是什么? MyBatis 是一款一流的支持自定义 SQL.存储过程和高级映射的持久化框架.MyBatis 几乎消除了所有的 JDBC 代码,也基本不需要手工去设置参数和获取检索结果.MyBatis 能够使用简单的XML 格式或者注解进行来配置,能够映射基本