多年不用
PageHelper
了,最近新入职的公司,采用了此工具集成的框架,作为一个独立紧急项目开发的基础。项目开发起来,还是手到擒来的,但是没想到,最终测试的时候,
深深的给我上了一课
。
一切的问题都要从我接受的项目开始说起, 在开发这个项目的过程中,发生了各种奇葩的事情, 下面我简单说给你们听听:
你肯定在想这是什么意思? 就是字面意思,已经注册的账号,可以再次注册成功!!!
else if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(username)) ||"匿名用户" .equals(username)){ // 注册用户已存在 msg = "注册用户'" + username + "'失败" ; }
如上所示:
checkUserNameUnique(username)
用来验证数据库是否存在用户名:
"checkUserNameUnique" parameterType="String" resultType="int"> select count(1) from sys_user where user_name = #{userName} limit 1
正常来说,是不会有问题的,那么原因我们后面讲,接着看下一个问题。
如上所示,明明有十多个结果,怎么只能返回5个?我也没有添加分页参数啊?
相信用过PageHelper的同学已经知道问题出在哪里了。
当管理员在后台界面重置用户的密码的时候,居然报错了??
报错信息清晰的告诉了我:
sql语句异常,update语句不认识 “Limit 5”
到此为止,报错信息已经告诉了我,我的
sql被拼接了该死的“limit”分页参数
。
上面提到的几个只是冰山一角,在我使用的过程中,还有各种涉及到sql的地方,会因为这个分页参数导致的问题,我可以分为两种:
比如insert、update语句等,不支持limit,会直接报错。
如我上面提到的用户可以重复注册,却没有报错,实际在代码当中是有报错的,但是当前方法对异常进行了throw,最终被全局异常捕获了。
不分页的sql被拼接了limit,导致没有报错,但是数据返回量错误。
注意:
异常不是每次出现,是有一定纪律的,但是触发几率较高
,原因在后面会逐渐脱出。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
我这里只讲解项目基于的框架的使用方式。
代码如下:
@GetMapping ("/cms/cmsEssayList" )public TableDataInfo cmsEssayList (CmsBlog cmsBlog) { //状态为发布 cmsBlog.setStatus("1" ); startPage(); List list = cmsBlogService.selectCmsBlogList(cmsBlog); return getDataTable(list); }
使用起来还是很简单的,通过
startPage()
指定分页参数,通过
getDataTable(list)
对结果数据封装成分页的格式。
有些同学会问,这也没没传分页参数啊,并且实体类当中也没有,这就是比较有意思的点,下一小结就来聊聊源码。
protected void startPage () { // 通过request去获取前端传递的分页参数,不需控制器要显示接收 PageDomain pageDomain = TableSupport.buildPageRequest(); Integer pageNum = pageDomain.getPageNum(); Integer pageSize = pageDomain.getPageSize(); if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize)) { String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); Boolean reasonable = pageDomain.getReasonable(); // 真正使用pageHelper进行分页的位置 PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable); } }
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable)
的参数分别是:
reasonable:分页合理化,对于不合理的分页参数自动处理,比如传递pageNum是小于0,会默认设置为1.
继续跟踪,连续点击startpage构造方法到达如下位置:
/** * 开始分页 * * @param pageNum 页码 * @param pageSize 每页显示数量 * @param count 是否进行count查询 * @param reasonable 分页合理化,null时用默认配置 * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置 */ public static Page startPage (int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); // 1、获取本地分页 Page oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } // 2、设置本地分页 setLocalPage(page); return page; }
到达终点位置了,分别是:
getLocalPage()
和
setLocalPage(page)
,分别来看下:
进入方法:
/** * 获取 Page 参数 * * @return */ public static Page getLocalPage () { return LOCAL_PAGE.get(); }
看看常量
LOCAL_PAGE
是个什么路数?
protected static final ThreadLocal LOCAL_PAGE = new ThreadLocal();
好家伙,是
ThreadLocal
,学过java基础的都知道吧,独属于每个线程的本地缓存对象。
当一个请求来的时候,会获取持有当前请求的线程的ThreadLocal,调用
LOCAL_PAGE.get()
,查看当前线程是否有未执行的分页配置。
此方法显而易见,设置线程的分页配置:
protected static void setLocalPage (Page page) { LOCAL_PAGE.set(page); }
经过前面的分析,我们发现,问题似乎就是这个ThreadLocal导致的。
是否在使用完之后没有进行清理?导致下一次此线程再次处理请求时,还在使用之前的配置?
我们带着疑问,看看mybatis时如何使用pageHelper的。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud
视频教程:https://doc.iocoder.cn/video/
我们需要关注的就是mybatis在何时使用的这个ThreadLocal,也就是何时将分页餐数获取到的。
前面提到过,通过PageHelper的
startPage()
方法进行page缓存的设置,当程序执行sql接口mapper的方法时,就会被拦截器
PageInterceptor
拦截到。
PageHelper其实就是mybatis的分页插件,其实现原理就是通过拦截器的方式,pageHelper通
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(); } } }
如上所示是intecept的全部代码,我们下面只关注几个终点位置:
此处的
skip方法
进行设置分页参数,内部调用方法:
Page page = pageParams.getPage(parameterObject, rowBounds);
继续跟踪
getPage()
,发现此方法的第一行就获取了ThreadLocal的值:
Page page = PageHelper.getLocalPage();
我们都知道,分页需要获取记录总数,所以,这个拦截器会在分页前先进行count操作。
如果count为0,则直接返回,不进行分页:
//处理查询总数,返回 true 时继续分页查询,false 时直接返回 if (!dialect.afterCount(count, parameter, rowBounds)) { //当查询总数为 0 时,直接返回空的结果 return dialect.afterPage(new ArrayList(), parameter, rowBounds); }
afterPage其实是对分页结果的封装方法,即使不分页,也会执行,只不过返回空列表。
在处理完count方法后,就是真正的进行分页了:
resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
此方法在执行分页之前,会判断是否执行分页,依据就是前面我们通过ThreadLocal的获取的page。