前言
mybatis是个优秀的半自动ORM框架,国内使用的特别多。增强版本的有plus,在开发过程对于数据量多的时候会使用分页。而PageHelper是一个很不错的分页插件,我们不用直接写count的sql,不用进行limit offset等的编写,只需要调用少量方法就可以进行分页,十分方便,提高开发效率。和Mybatis Generator、Mybatis Plugin 并称为mybatis三剑客。
使用方式
首先我们导入maven依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.4</version>
</dependency>
SpringMVC
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageHelper">
<property name="properties">
<!-- 什么都不配,使用默认的配置 -->
<value>
dialect=mysql
</value>
</property>
</bean>
</array>
</property>
</bean>
SpringBoot
,需要通过new一个SqlSessionFactoryBean,然后setPlugins,再build生成sqlSessionFactory就可以了。
我们先看看如何使用,如下
PageHelper.startPage(page.getPageNo(),page.getPageSize());
List<CollectionVO> data1 = collectionMapper.selectByUser(userId,page);
PageInfo<CollectionVO> pageInfo1 = new PageInfo<>(data1);
只需要PageHelper.startPage
设置分页的规则,最后用PageInfo
包装查询的结果就完成了分页,是不是很方便呢?
源码解析
Mybatis SqlSessionFactoryBean plugins
设置com.github.pagehelper.PageHelper
我们可以看到PageHelper实现了Interceptor接口,这是mybatis为扩展插件的预留接口,实现这个接口编写插件逻辑,并set到plugins,就可以在mybatis执行的时候执行插件中的内容,我们继续往下看。
那么如何使用这个插件呢?
PageHelper.startPage(int pageNum, int pageSize)
在我们需要分页的地方之前,在mapper查询之前使用,PageHelper设置当前的页码和页大小。
我们进入startPage
这个方法,可以看到,使用的是构造器模式
,建造者模式的前身
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
//创建一个分页对象,保存页码,是否统计,页码,排序等规则
Page<E> page = new Page(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
Page<E> oldPage = SqlUtil.getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
//设置排序规则
page.setOrderBy(oldPage.getOrderBy());
}
//将当前实例化的Page对象放入ThreadLocal,这样在执行的时候可以取到,计算分页的offset和limit
SqlUtil.setLocalPage(page);
return page;
}
我们进去看看Page,这里贴出部分代码,我们可以看到这里都是用来记录我们对于分页的一些设置,排序规则,是否统计总数等等。
public class Page<E> extends ArrayList<E> {
private static final long serialVersionUID = 1L;
private int pageNum;
private int pageSize;
private int startRow;
private int endRow;
private long total;
private int pages;
private boolean count;
private Boolean countSignal;
private String orderBy;
private boolean orderByOnly;
private Boolean reasonable;
private Boolean pageSizeZero;
public Page() {
}
.......
}
在startPage之后生成了对应的Page,并通过SqlUtil.setLocalPage(page)
放入ThreadLocal
中,这样之后可以通过ThreadLocal
获取
接下来是调用mapper.xxxx()
PageHelper.startPage(page.getPageNo(),page.getPageSize());
List<CollectionVO> data1 = collectionMapper.selectByUser(userId,page);
PageInfo<CollectionVO> pageInfo1 = new PageInfo<>(data1);
mybatis的执行我们知道是通过Executor执行的,底层是原生的PrepareStatement这些,关于mybatis的源码这里就不详细说明了,感兴趣的可以去阅读,也是必读源码之一。
我们直接进入主题,进入到org.apache.ibatis.session.Configuration
类,看到如下方法
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//三种基本的执行器
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//这里CacheExecutor是对前面的进行装饰增强,装饰者模式
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//重点,这里加载插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
接下来我们进入到org.apache.ibatis.plugin.InterceptorChain
的如下方法
public Object pluginAll(Object target) {
//遍历所有的插件,如果你要问我interceptors集合的数据在哪里初始化的,看下面
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
关于interceptors的初始化,我们可以看到org.mybatis.spring.SqlSessionFactoryBean
的buildSqlSessionFactory
放下,由于代码过多,这里只截取有关的,如下
//判断插件列表时候为空,this.plugins不就是我们在构造SqlSessionFactoryBean的时候放入的吗,通过Spring去loadBeanDefinition,注入到SqlSessionFactoryBean实例的属性中,这里可能需要稍微了解一下Spring源码
if (!isEmpty(this.plugins)) {
//遍历所有的插件
for (Interceptor plugin : this.plugins) {
//加入到列表
configuration.addInterceptor(plugin);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered plugin: '" + plugin + "'");
}
}
}
继续进入configuration.addInterceptor(plugin)
,org.apache.ibatis.session.Configuration的addInterceptor方法,可以看到调用了InterceptorChain的addInterceptor方法,所以interceptors会包含我们PageHelper这个插件了。回归到主线,继续往下。
上述经过org.apache.ibatis.plugin.InterceptorChain.pluginAll
方法后,我们得到了一个执行器的代理对象,在sql执行的时候,就会把执行权交给代理对象。
这里在看下pluginAll
方法吧,进入com.github.pagehelper.PageHelper
的如下方法
public Object plugin(Object target) {
//目标对象是执行器,生成代理
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
进入org.apache.ibatis.plugin.Plugin
的wrap
方法
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//目标类类对象
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
//构造JDK代理,这里InvocationHandler是Plugin对象,那么最终执行器执行会被代理到Plugin的invoke方法中来,了解代理模式的应该都能明白。
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
那么接下来我们进入到org.apache.ibatis.plugin.Plugin.invoke
方法,如下
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
//看这里就好了。执行PageHelper的intercept方法,并将执行的参数,目标对象,方法等信息传递过去
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
回到com.github.pagehelper.PageHelper.intercept
,最终的处理都是在这里进行。
public Object intercept(Invocation invocation) throws Throwable {
if (autoRuntimeDialect) {
SqlUtil sqlUtil = getSqlUtil(invocation);
return sqlUtil.processPage(invocation);
} else {
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
}
由于我们只看主线,部分逻辑就没细讲了,进入到sqlUtil.processPage(invocation)
,继续进入_processPage(invocation)
,在进入doProcessPage(invocation, page, args)
,我们可以看到这里对相关sql进行了处理,首先我们看下总数查询如何做的。
/**
* Mybatis拦截器方法
*
* @param invocation 拦截器入参
* @return 返回执行结果
* @throws Throwable 抛出异常
*/
private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
//保存RowBounds状态
RowBounds rowBounds = (RowBounds) args[2];
//获取原始的ms
MappedStatement ms = (MappedStatement) args[0];
//判断并处理为PageSqlSource
if (!isPageSqlSource(ms)) {
//这里去builder一个count的MapperStatement,因为我们要查询总数,需要这样一个Statement去执行
processMappedStatement(ms);
}
//设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
((PageSqlSource)ms.getSqlSource()).setParser(parser);
try {
//忽略RowBounds-否则会进行Mybatis自带的内存分页
args[2] = RowBounds.DEFAULT;
//如果只进行排序 或 pageSizeZero的判断
if (isQueryOnly(page)) {
return doQueryOnly(page, invocation);
}
//简单的通过total的值来判断是否进行count查询
if (page.isCount()) {
page.setCountSignal(Boolean.TRUE);
//替换MS
args[0] = msCountMap.get(ms.getId());
//查询总数
//这里是去要去执行查询
Object result = invocation.proceed();
//还原ms
args[0] = ms;
//设置总数
page.setTotal((Integer) ((List) result).get(0));
if (page.getTotal() == 0) {
return page;
}
} else {
page.setTotal(-1l);
}
//pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
if (page.getPageSize() > 0 &&
((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
|| rowBounds != RowBounds.DEFAULT)) {
//将参数中的MappedStatement替换为新的qs
page.setCountSignal(null);
BoundSql boundSql = ms.getBoundSql(args[1]);
args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
page.setCountSignal(Boolean.FALSE);
//执行分页查询
Object result = invocation.proceed();
//得到处理结果
page.addAll((List) result);
}
} finally {
((PageSqlSource)ms.getSqlSource()).removeParser();
}
//返回结果
return page;
}
经过invocation.proceed()
我们会进入到org.apache.ibatis.executor.CachingExecutor.query
方法,接着我们进入getBoundSql(parameterObject)
进入org.apache.ibatis.mapping.MappedStatement.getBoundSql
接着进入到com.github.pagehelper.sqlsource.PageSqlSource
的如下方法
/**
* 获取BoundSql
*
* @param parameterObject
* @return
*/
@Override
public BoundSql getBoundSql(Object parameterObject) {
Boolean count = getCount();
if (count == null) {
return getDefaultBoundSql(parameterObject);
} else if (count) {
//生成count的sql,我们进去这里
return getCountBoundSql(parameterObject);
} else {
return getPageBoundSql(parameterObject);
}
}
进入getCountBoundSql
,继续点进去,我们最终会进入到com.github.pagehelper.parser.SqlParser
的getSmartCountSql
方法,如下
public String getSmartCountSql(String sql) {
//校验是否支持该sql
isSupportedSql(sql);
//如果已经缓存过sql,直接返回
if (CACHE.get(sql) != null) {
return CACHE.get(sql);
}
//解析SQL
Statement stmt = null;
try {
//解析器解析sql,这里的解析器用的是jsqlparser,具体的解析器就不剖析了。
stmt = CCJSqlParserUtil.parse(sql);
} catch (Throwable e) {
//无法解析的用一般方法返回count语句
String countSql = getSimpleCountSql(sql);
CACHE.put(sql, countSql);
return countSql;
}
//解析之后会得到一个Select对象,将sql每部分解析填充到Select对象的属性中
Select select = (Select) stmt;
SelectBody selectBody = select.getSelectBody();
//处理body-去order by
processSelectBody(selectBody);
//处理with-去order by
processWithItemsList(select.getWithItemsList());
//处理为count查询
//这里会对sql加上count(0)和别名
sqlToCount(select);
String result = select.toString();
CACHE.put(sql, result);
//最终的sql就是select count(0) from (xxxxxxx) table_alias
return result;
}
回到com.github.pagehelper.SqlUtil.doProcessPage
方法,如下,得到result就是包含总数的一个list,取第一项set到Page对象的total属性
if (page.isCount()) {
page.setCountSignal(Boolean.TRUE);
//替换MS
args[0] = msCountMap.get(ms.getId());
//查询总数
//这里是去要去执行查询
Object result = invocation.proceed();
//还原ms
args[0] = ms;
//设置总数
page.setTotal((Integer) ((List) result).get(0));
if (page.getTotal() == 0) {
return page;
}
} else {
page.setTotal(-1l);
}
查询的列表结果在如下流程,就是执行原sql,然后得到List结果集,加入到Page中,Page是个List,继承ArrayList
//pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
if (page.getPageSize() > 0 &&
((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
|| rowBounds != RowBounds.DEFAULT)) {
//将参数中的MappedStatement替换为新的qs
page.setCountSignal(null);
BoundSql boundSql = ms.getBoundSql(args[1]);
args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
page.setCountSignal(Boolean.FALSE);
//执行分页查询
Object result = invocation.proceed();
//得到处理结果
page.addAll((List) result);
}
最终我们就可以得到一个有总数和结果的Page对象了,也就是mapper.xxxxx()方法返回的数据,如下,我们可以看到执行后返回的不是List,而是一个Page对象
这个时候我们用PageInfo格式化一下,得到给前端的分页信息,至此,整个流程结束。