多年以后,PageHelper 又深深给我上了一课!

Score Matching(得分匹配) Score Matching(得分匹配)是一种统计学习方法,用于估计概率密度函数的梯度(即得分函数),而无需知道密度函数的归一化常数。这种方法由Hyvärinen在2005年提出,主要用于无监督学习,特别是在密度估计和生成模型中。 阅读详情

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

129a20c8cfdca6b55bba27279b16369b.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、CRM 等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 

来源:juejin.cn/post/
7125356642366914596


多年不用PageHelper了,最近新入职的公司,采用了此工具集成的框架,作为一个独立紧急项目开发的基础。项目开发起来,还是手到擒来的,但是没想到,最终测试的时候,深深的给我上了一课。

我的项目发生了哪些奇葩现象?

一切的问题都要从我接受的项目开始说起, 在开发这个项目的过程中,发生了各种奇葩的事情, 下面我简单说给你们听听:

账号重复注册?

你肯定在想这是什么意思? 就是字面意思,已经注册的账号,可以再次注册成功!!!

else if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(username))
||"匿名用户".equals(username)){
    // 注册用户已存在
    msg = "注册用户'" + username + "'失败";
}

如上所示: checkUserNameUnique(username)用来验证数据库是否存在用户名:

<select id="checkUserNameUnique" parameterType="String" resultType="int">
   select count(1) from sys_user where user_name = #{userName} limit 1
</select>

正常来说,是不会有问题的,那么原因我们后面讲,接着看下一个问题。

查询全部分类的下拉列表只能查出5条数据?

c9ceed79a243f0d6915ce0ade2ab5011.jpeg

如上所示,明明有十多个结果,怎么只能返回5个?我也没有添加分页参数啊?

相信用过PageHelper的同学已经知道问题出在哪里了。

修改用户密码报错?

当管理员在后台界面重置用户的密码的时候,居然报错了??

报错信息清晰的告诉了我:sql语句异常,update语句不认识 “Limit 5”

到此为止,报错信息已经告诉了我,我的sql被拼接了该死的“limit”分页参数。

小结

上面提到的几个只是冰山一角,在我使用的过程中,还有各种涉及到sql的地方,会因为这个分页参数导致的问题,我可以分为两种:

  • 1)直接导致报错的:明确报错原因的

比如insert、update语句等,不支持limit,会直接报错。

  • 2)导致业务逻辑错误,但是代码没有错误提示

如我上面提到的用户可以重复注册,却没有报错,实际在代码当中是有报错的,但是当前方法对异常进行了throw,最终被全局异常捕获了。

不分页的sql被拼接了limit,导致没有报错,但是数据返回量错误。

注意:异常不是每次出现,是有一定纪律的,但是触发几率较高 ,原因在后面会逐渐脱出。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

PageHelper是怎么做到上面的问题的?

PageHelper使用

我这里只讲解项目基于的框架的使用方式。

代码如下:

@GetMapping("/cms/cmsEssayList")
public TableDataInfo cmsEssayList(CmsBlog cmsBlog) {
    //状态为发布
    cmsBlog.setStatus("1");
    startPage();
    List<CmsBlog> list = cmsBlogService.selectCmsBlogList(cmsBlog);
    return getDataTable(list);
}

使用起来还是很简单的,通过 startPage()指定分页参数,通过getDataTable(list)对结果数据封装成分页的格式。

有些同学会问,这也没没传分页参数啊,并且实体类当中也没有,这就是比较有意思的点,下一小结就来聊聊源码。

startPage()干啥了?

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)的参数分别是:

  • 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()

进入方法:

/**
 * 获取 Page 参数
 *
 * @return
 */
public static <T> Page<T> getLocalPage() {
    return LOCAL_PAGE.get();
}

看看常量LOCAL_PAGE是个什么路数?

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

好家伙,是ThreadLocal,学过java基础的都知道吧,独属于每个线程的本地缓存对象。

当一个请求来的时候,会获取持有当前请求的线程的ThreadLocal,调用LOCAL_PAGE.get(),查看当前线程是否有未执行的分页配置。

setLocalPage(page)

此方法显而易见,设置线程的分页配置:

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使用pageHelper分析

我们需要关注的就是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的全部代码,我们下面只关注几个终点位置:

设置分页:dialect.skip(ms, parameter, rowBounds)

此处的skip方法进行设置分页参数,内部调用方法:

Page page = pageParams.getPage(parameterObject, rowBounds);

继续跟踪getPage(),发现此方法的第一行就获取了ThreadLocal的值:

Page page = PageHelper.getLocalPage();

统计数量:dialect.beforeCount(ms, parameter, rowBounds)

我们都知道,分页需要获取记录总数,所以,这个拦截器会在分页前先进行count操作。

如果count为0,则直接返回,不进行分页:

//处理查询总数,返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
    //当查询总数为 0 时,直接返回空的结果
    return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}

afterPage其实是对分页结果的封装方法,即使不分页,也会执行,只不过返回空列表。

分页:ExecutorUtil.pageQuery

在处理完count方法后,就是真正的进行分页了:

resultList = ExecutorUtil.pageQuery(dialect, executor,
        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);

此方法在执行分页之前,会判断是否执行分页,依据就是前面我们通过ThreadLocal的获取的page。

当然,不分页的查询,以及新增和更新不会走到这个方法当中。

非分页:executor.query

而是会走到下面的这个分支:

resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);

我们可以思考一下,如果ThreadLoad在使用后没有被清除,当执行非分页的方法时,那么就会将Limit拼接到sql后面。

为什么不分也得也会拼接?我们回头看下前面提到的dialect.skip(ms, parameter, rowBounds):

bbc8b691258c2b2a2838436f3fe7cdfb.jpeg

如上所示,只要page被获取到了,那么这个sql,就会走前面提到的ExecutorUtil.pageQuery分页逻辑,最终导致出现不可预料的情况。

其实PageHelper对于分页后的ThreaLocal是有清除处理的。

清除TheadLocal

在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的使用方式就讲解完了。

整体看下来,似乎不会存在什么问题,但是我们可以考虑集中极端情况:

  • 如果使用了startPage(),但是没有执行对应的sql,那么就表明,当前线程ThreadLocal被设置了分页参数,可是没有被使用,当下一个使用此线程的请求来时,就会出现问题。

  • 如果程序在执行sql前,发生异常了,就没办法执行finally当中的clearPage()方法,也会造成线程的ThreadLocal被污染。

所以,官方给我们的建议,在使用PageHelper进行分页时,执行sql的代码要紧跟startPage()方法 。

除此之外,我们可以手动调用clearPage()方法 ,在存在问题的方法之前。

需要注意:不要分页的方法前手动调用clearPage,将会导致你的分页出现问题 。

还有人问为什么不是每次请求都出错?

这个其实取决于我们启动服务所使用的容器,比如tomcat,在其内部处理请求是通过线程池的方式。甚至现在的很多容器是基于netty的,都是通过线程池,复用线程来增加服务的并发量。

假设线程1持有没有被清除的page参数,不断调用同一个方法,后面两个请求使用的是线程2和线程3没有问题,再一个请求轮到线程1了,此时就会出现问题了。

总结

关于PageHelper的介绍就这么多,真的是折磨我好几天,要不是项目紧急,来不及替换,我一定不会使用这个组件。

莫名其妙的就会有个方法出现问题,一通排查,发现都是这个PageHelper导致的。虽然我已经全局搜索使用的地方,保证startPage()后紧跟sql命令,但是仍然有嫌犯潜逃,只能在有问题的方法使用clearPage()来打补丁。

虽然PageHelper给我带来一些困扰,耗费了一定的时间,但是定位问题的过程中,也学习了mybatis和pagehepler的实现方式,对于热爱源码阅读的同学来说还是有一定的提升的。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,“长按”或“扫描”下方二维码噢:

ada279c61e18ab33e727f1bbe05755a8.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

4ef594ab0263334158ea6ce5105839f6.png

0b85b97fe0bd6fe5d647ff6b48f7a0e7.pngc7f175102279dccf7e7656422325f141.pnge2b32c14242130cc38d1c2c92d076b3b.png326c4a67b22ce0ad66962c47def1c84e.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
告别 GENERATE_SUBPOOL_DIR_FULL:用 RTTC 平替 CL_ALV_TABLE_CREATE=>CREATE_DYNAMIC_TABLE 的一套更稳方案 文章摘要:本文探讨了ABAP开发中动态内表的创建问题,重点分析了使用CL_ALV_TABLE_CREATE方法可能遇到的GENERATE_SUBPOOL_DIR_FULL错误。该错误源于临时subroutine pool的36次创建上限,常在批量处理或频繁调用时出现。文章提出基于RTTC技术的替代方案ZCL_ALV_TABLE_CREATE_RTTC,通过运行时类型服务直接创建动态内表,无需生成临时代码,从而规避了资源限制。该方法保持接口兼容性,只需简单替换类名即可实现无缝迁移,是解决动态表创建瓶颈的有效方 阅读详情

相关推荐

LTE/NR中的相应cause场景【原创】

目录1.背景2.LTE EMM cause2.0 写在最前2.1 特殊处理的cause第一梯队第二梯队2.2 非特殊处理的cause3.NR cause 实网分析终端被拒等问题,经常遇到各类cause,其会对应不同的终端限制行为。查找已有资料后理解没有很深,自行总结本文用于梳理现象,如有错误敬请指正,转载请注明出处。 本文用于记录协议Ts 24.301及Ts 24.501中的cause及其对应行为。 已总结的cause值: 24.301中的cause分

2301_79567300的博客 3499

Spring Boot集成Mybatis Plus通过Pagehelper实现分页查询

Spring Boot 引入 starter 后自动生效,对分页插件进行配置时,在 Spring Boot 对应的配置文件 application.[properties|yaml] 中配置。配置的字段中取值,查找到合适的值时就会自动分页。方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置。正常情况,没有分页是有以张开头有三条数据,有分页的情况下 张1,张2。返回结果有疑问:返回的是没有分页,只要是张开头的数据都返回了。,分页插件会从查询方法的参数值中,自动根据上面。

极客鼠之家 5536

无法找到 Adobe PDF资源文件。“Acrobat PDFMaker“您必须具有”管理员”权限方可安装这些文件...错误怎么解决

本文的内容是无法找到 Adobe PDF资源文件。“Acrobat PDFMaker“您必须具有”管理员”权限方可安装这些文件...错误的解决办法。

西岸贤 1万+

智能提醒助理系列-jdk8升级到21,springboot2.3升级到3.3

智能提醒小程序 当前使用的是jdk8,springboot2.3,升级到jdk21和springboot3.3 学习新知识的同时,感受带来的性能提升。

涛哥的专栏 1303

大数据领域数据建模的多源数据融合技术

在大数据时代,数据来源呈现出多样化的特点,包括企业内部的不同业务系统数据、外部的社交媒体数据、传感器数据等。这些多源数据蕴含着丰富的信息,但由于数据格式、语义、质量等方面的差异,难以直接进行分析和利用。多源数据融合技术的目的就是将这些来自不同数据源的数据进行整合、清洗、关联和统一,构建一个具有一致性和完整性的数据模型,以便更好地挖掘数据背后的价值,为企业决策、科学研究等提供有力支持。

Java大师兄的博客 1306

ArcGIS图解建模工具与DEM提取山脊、山谷线(水文分析、表面分析)

参考汤国安、杨昕等编著的《地理信息系统空间分析实验教程第二版》 利用水文分析(流量)和表面分析(坡向变率)得到山脊和山谷线 文章目录1、基本概念1.1 正地形1.2 负地形正负地形的影响2、水文分析方法2.1 基本思想原理2.2 技术路线2.3 操作步骤2.3.1 正负地形的提取2.3.2 利用水文分析思想和工具提取研究区域的山脊线2.3.3 利用水文分析思想和工具提取研究区域的山谷线2.3.4 利用水文分析提取山脊线和山谷线结果3、表面分析方法3.1 基本思想原理3.2 技术路线3.3 操作步骤4、图.

fungis 1万+

Ruoyi-Vue 升级JDK21、Springboot3、Mybatis3

RUOYI升级jdk21 springboot3

andy7_的博客 1364

表面声波滤波器——叉指换能器设计实战:从关键参数到结构选型

本文深入探讨了表面声波滤波器核心部件叉指换能器的实战设计流程。从目标驱动出发,详细解析了如何根据中心频率确定关键参数指宽,并权衡叉指对数对带宽和插入损耗的影响。文章进一步介绍了通过变迹加权优化频率响应,并对比了梯形结构与纵向耦合谐振滤波器等主流结构选型的适用场景与优缺点,为工程师提供从理论计算到仿真验证的完整设计指南。

weixin_29216353的博客 105

多年以后,PageHelper 又深深的给我上了一课!

戳上方蓝字“Java笔记虾”关注我多年不用PageHelper了,最近新入职的公司,采用了此工具集成的框架,作为一个独立紧急项目开发的基础。项目开发起来,还是手到擒来的,但是没想到,最终测试的时候,深深的给我上了一课。我的项目发生了哪些奇葩现象?一切的问题都要从我接受的项目开始说起, 在开发这个项目的过程中,发生了各种奇葩的事情, 下面我简单说给你们听听:账号重复注册?你肯定在想这是什么意思? 就...

Java笔记虾 199

FPGA实现串口升级及MultiBoot(九)BPI FLASH相关实例演示

与SPI相关知识并没有什么太多区别(别杠硬件或者架构上),相关知识点都在上面指出,大家有什么其他需要注意点,欢迎留言讨论。关于“四样错误”就不演示了,没什么太大区别,下一讲我们开始串口升级。PS:为了扩展ICAPE2原语的使用,在原来的基础上增加了几个常用FPGA ID,并在触发启动时候增加了可选电平触发或上升/下降沿触发(例化IP时选择),新版本号V1.1。

OpenFPGA的博客 1445

测风数据垂直外推(原理及Windowgrapher操作示范)

垂直外推

weixin_46713695的博客 897

多年以后【PageHelper】又深深的给我上了一课!!

关于PageHelper的介绍就这么多,真的是折磨我好几天,要不是项目紧急,来不及替换,我一定不会使用这个组件。莫名其妙的就会有个方法出现问题,一通排查,发现都是这个PageHelper导致的。虽然我已经全局搜索使用的地方,保证startPage()后紧跟sql命令,但是仍然有嫌犯潜逃,只能在有问题的方法使用clearPage()来打补丁。

jcc4261的博客 382

2020年大连理工大学《计算机控制技术》期末试卷

2020年大连理工大学《计算机控制技术》期末试卷 2020年大连理工大学《计算机控制技术》期末试卷 2020年大连理工大学《计算机控制技术》期末试卷 2020年大连理工大学《计算机控制技术》期末试卷 ...

无名何许人的博客 650

多年以后「PageHelper」又深深的给我上了一课

多年不用PageHelper了,最近新入职的公司,采用了此工具集成的框架,作为一个独立紧急项目开发的基础。项目开发起来,还是手到擒来的,但是没想到,最终测试的时候,深深的给我上了一课。

YYniannian的博客 471

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

PageHelper 又给我上了一课!

关于PageHelper的介绍就这么多,真的是折磨我好几天,要不是项目紧急,来不及替换,我一定不会使用这个组件。莫名其妙的就会有个方法出现问题,一通排查,发现都是这个PageHelper导致的。虽然我已经全局搜索使用的地方,保证startPage()后紧跟sql命令,但是仍然有嫌犯潜逃,只能在有问题的方法使用clearPage()来打补丁。

Wewe的博客 923

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

上一篇: 离职之后四战美团,终于拿到Offer!
下一篇: 今年面试好激烈!
公众号-芋道源码
博客等级 码龄10年 1634粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值