PageHelper插件介绍
1、使用说明
1)引入pagehelper依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.2<ersion>
</dependency>
2)配置applicationContext.xml文件
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<value>
<!-- 这里设定你的数据库类型 -->
helperDialect=mysql
</value>
</property>
</bean>
</array>
</property>
</bean>
3)调用PageHelper的方法
在service方法中调用PageHelper的静态方法
startPage(注意一定要在实际查询数据库之前调用该方法,且只对查库的第一个方法有效),传入需要查询的页号和每页大小,返回PageHelper插件提供的PageInfo对象。即可自动完成数据库物理分页,无须在你的sql语句中手工加
limit子句
2、源码分析
通过该自定义拦截器并在其中拦截Executor.query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)方法或Executor.query( MappedStatement, Object, RowBounds, ResultHandler)方法。在拦截的Executor.query()方法中,可以通过RowBounds参数获取所需记录的起止位置,通过BoundSql参数获取待执行的SQL语句,这样就可以在SQL语句中合适的位置添加“limit offset, length”片段,实现分页功能。先展示PageInterceptor类代码,然后再说明:
package com.github.pagehelper;
import com.github.pagehelper.cache.Cache;
import com.github.pagehelper.cache.CacheFactory;
import com.github.pagehelper.util.MSUtils;
import com.github.pagehelper.util.StringUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Mybatis - 通用分页拦截器<br/>
* 项目地址 : http://git.oschina.net/free/Mybatis_PageHelper
*
* 通过该自定义拦截器并在其中拦截Executor.query(MappedStatement,Object,RowBounds,ResultHandler,CacheKey,BoundSql)
* 方法或Executor.query(MappedStatement,Object,RowBounds,ResultHandler)方法。在拦截的Executor.query()方法中,可以
* 通过RowBounds参数获取所需记录的起止位置,通过BoundSql参数获取待执行的SQL语句,这样就可以在SQL语句中合适的位置添加“limit offset,length”
* 片段,实现分页功能。
*
* @author liuzh/abel533/isea533
* @version 5.0.0
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts(
{ // 需要拦截的类/方法/入参
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor {
//缓存count查询的ms
protected Cache<CacheKey, MappedStatement> msCountMap = null;
//Dialect对象。每种数据库产品对象
private Dialect dialect;
private String default_dialect_class = "com.github.pagehelper.PageHelper";
private Field additionalParametersField;// additional:额外
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
// 从Invocation对象中获取被拦截的方法的参数列表,这里就是
// Executor.query(MappedStatement,Object,RowBounds,ResultHandler)的参数列表
Object[] args = invocation.getArgs();
// 获取MappedStatement对象
MappedStatement ms = (MappedStatement) args[0];
// 获取用户传入的实参对象
Object parameter = args[1];
// 获取RowBounds对象
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if(args.length == 4){
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//反射获取动态参数
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//创建 count 查询的缓存 key
CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
countKey.update(MSUtils.COUNT);
MappedStatement countMs = msCountMap.get(countKey);
if (countMs == null) {
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
countMs = MSUtils.newCountMappedStatement(ms);
msCountMap.put(countKey, countMs);
}
//调用方言获取 count sql
String countSql = dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey);
countKey.update(countSql);
BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
//当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
for (String key : additionalParameters.keySet()) {
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行 count 查询
Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
Long count = (Long) ((List) countResultList).get(0);
//处理查询总数
//返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
//判断是否需要进行分页查询
if (dialect.beforePage(ms, parameter, rowBounds)) {
//生成分页的缓存 key
CacheKey pageKey = cacheKey;
//处理参数对象
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
//调用方言获取分页 sql
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
//设置动态参数
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行分页查询
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不执行分页的情况下,也不执行内存分页
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
} else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
dialect.afterAll();
}
}
@Override
public Object plugin(Object target) {
//TODO Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化,因此考虑在这个方法中做一次判断和初始化
//TODO https://github.com/pagehelper/Mybatis-PageHelper/issues/26
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
//缓存 count ms
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
String dialectClass = properties.getProperty("dialect");
if (StringUtil.isEmpty(dialectClass)) {
dialectClass = default_dialect_class;
}
try {
Class<?> aClass = Class.forName(dialectClass);
dialect = (Dialect) aClass.newInstance();
} catch (Exception e) {
throw new PageException(e);
}
dialect.setProperties(properties);
try {
//反射获取 BoundSql 中的 additionalParameters 属性
additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
additionalParametersField.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new PageException(e);
}
}
}
通过以上代码,分析说明如下:
- 初始化设置方法setProperties,会根据配置的
helperDialect=mysql
来选择MySqlDialect.java方言。 - 分页查询前会多做一次select count(*) from table的查询,其生成了新的MappedStatement来实现。
- 该工具架构存在优化空间。当使用“limit offset,length”方式实现分页遇到性能问题时,可以根据实际的业务需求,考虑在MyBatis的用户自定义插件中,将相关limit语句实现的分页功能修改。大概实现思路为:通过子查询来找到聚簇索引,再通过聚簇索引执行主查询。