明明有十多个结果,怎么只能返回5个?
当管理员在后台界面重置用户的密码的时候,居然报错了?
报错信息:sql中update语句不认识 “Limit 5”
可想而知,我的sql被拼接了“limit”分页参数!!!
我首先得讲解一下基本使用。
代码如下:
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable)
的参数分别是:
pageNum
:页数
pageSize
:每页数据量
orderBy
:排序
reasonable
:分页合理化,对于不合理的分页参数自动处理,比如传递pageNum是小于0,会默认设置为1.
连续点击startpage
构造方法到达如下位置:
/**
?*?开始分页
?*?@param?pageNum??????页码
?*?@param?pageSize?????每页显示数量
?*?@param?count????????是否进行count查询
?*?@param?reasonable???分页合理化,null时用默认配置
?*?@param?pageSizeZero?true且pageSize=0时返回全部结果,false时分页,null时用默认配置
?*/
public?static?<E>?Page<E>?startPage(int?pageNum,?int?pageSize,?boolean?count,?Boolean?reasonable,?Boolean?pageSizeZero)?{
????Page<E>?page?=?new?Page<E>(pageNum,?pageSize,?count);
????page.setReasonable(reasonable);
????page.setPageSizeZero(pageSizeZero);
????//?1、获取本地分页
????Page<E>?oldPage?=?getLocalPage();
????if?(oldPage?!=?null?&&?oldPage.isOrderByOnly())?{
????????page.setOrderBy(oldPage.getOrderBy());
????}
?????//?2、设置本地分页
????setLocalPage(page);
????return?page;
}
到达终点位置了,分别是:getLocalPage()
和setLocalPage(page)
,分别来看下:
getLocalPage()
进入方法:
看看常量LOCAL_PAGE
是个什么路数?
当一个请求来的时候,会获取持有当前请求的线程的ThreadLocal,调用LOCAL_PAGE.get()
,查看当前线程是否有未执行的分页配置。
setLocalPage(page)
设置线程的分页配置:
经过前面的分析,我们发现,问题似乎就是这个ThreadLocal导致的。
是否在使用完之后没有进行清理?导致下一次此线程再次处理请求时,还在使用之前的配置?
前面提到过,通过PageHelper的startPage()
方法进行page缓存的设置,当程序执行sql接口mapper的方法时,就会被拦截器PageInterceptor
拦截到。
我们只关注intercept方法:
@Override
public?Object?intercept(Invocation?invocation)?throws?Throwable?{
????try?{
????????Object[]?args?=?invocation.getArgs();
????????MappedStatement?ms?=?(MappedStatement)?args[0];
????????Object?parameter?=?args[1];
????????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];
????????}
????????checkDialectExists();
????????//对?boundSql?的拦截处理
????????if?(dialect?instanceof?BoundSqlInterceptor.Chain)?{
????????????boundSql?=?((BoundSqlInterceptor.Chain)?dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL,?boundSql,?cacheKey);
????????}
????????List?resultList;
????????//调用方法判断是否需要进行分页,如果不需要,直接返回结果
????????if?(!dialect.skip(ms,?parameter,?rowBounds))?{
????????????//判断是否需要进行?count?查询
????????????if?(dialect.beforeCount(ms,?parameter,?rowBounds))?{
????????????????//查询总数
????????????????Long?count?=?count(executor,?ms,?parameter,?rowBounds,?null,?boundSql);
????????????????//处理查询总数,返回?true?时继续分页查询,false?时直接返回
????????????????if?(!dialect.afterCount(count,?parameter,?rowBounds))?{
????????????????????//当查询总数为?0?时,直接返回空的结果
????????????????????return?dialect.afterPage(new?ArrayList(),?parameter,?rowBounds);
????????????????}
????????????}
????????????resultList?=?ExecutorUtil.pageQuery(dialect,?executor,
????????????????????ms,?parameter,?rowBounds,?resultHandler,?boundSql,?cacheKey);
????????}?else?{
????????????//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
????????????resultList?=?executor.query(ms,?parameter,?rowBounds,?resultHandler,?cacheKey,?boundSql);
????????}
????????return?dialect.afterPage(resultList,?parameter,?rowBounds);
????}?finally?{
????????if(dialect?!=?null){
????????????dialect.afterAll();
????????}
????}
}
我们只需要关注几个终点位置:
此处的skip方法进行设置分页参数,内部调用方法:
继续跟踪getPage()
,发现此方法的第一行就获取了ThreadLocal的值:
Page?page?=?PageHelper.getLocalPage();
我们都知道,分页需要获取记录总数,所以,这个拦截器会在分页前先进行count操作。
如果count为0,则直接返回,不进行分页:
afterPage其实是对分页结果的封装方法,即使不分页,也会执行,只不过返回空列表。
分页:ExecutorUtil.pageQuery
在处理完count方法后,就是真正的进行分页了:
此方法在执行分页之前,会判断是否执行分页,依据就是前面我们通过ThreadLocal的获取的page。
当然,不分页的查询,以及新增和更新不会走到这个方法当中。
非分页:executor.query
而是会走到下面的这个分支:
resultList?=?executor.query(ms,?parameter,?rowBounds,?resultHandler,?cacheKey,?boundSql);
我们可以思考一下,如果ThreadLoad在使用后没有被清除,当执行非分页的方法时,那么就会将Limit拼接到sql后面。
为什么不分也得也会拼接?我们回头看下前面提到的dialect.skip(ms, parameter, rowBounds)
:
如上所示,只要page被获取到了,那么这个sql,就会走前面提到的ExecutorUtil.pageQuery
分页逻辑,最终导致出现不可预料的情况。
在intercept方法的最后,会在sql方法执行完成后,清理page缓存:
finally?{
????if(dialect?!=?null){
????????dialect.afterAll();
????}
}
看看这个afterAll()
方法:
@Override
public?void?afterAll()?{
????//这个方法即使不分页也会被执行,所以要判断?null
????AbstractHelperDialect?delegate?=?autoDialect.getDelegate();
????if?(delegate?!=?null)?{
????????delegate.afterAll();
????????autoDialect.clearDelegate();
????}
????clearPage();
}
只关注?clearPage()
:
/**
?*?移除本地变量
?*/
public?static?void?clearPage()?{
????LOCAL_PAGE.remove();
}
总结:
所以,在使用PageHelper进行分页时,执行sql的代码要紧跟startPage()
方法。我们还需要手动调用clearPage()
方法,在存在问题的方法之前。
求一键三连:点赞、转发、在看。
搜索公众号:woniuxgg,在公众号中回复:笔记?获得蜗牛为你精心准备的实战语雀笔记
回复面试、开发手册、有超赞的粉丝福利。