当前位置: 首页 > article >正文

PageHelper 分页total失效或当前页含数量不匹配问题解决方案 含原理分析

前言

开发过程中遇到的关于PageHelper失效的问题,一个是因为对PageHelper作用返回的list结果集进行二次操作导致的total总数等于查询到的数量。一个是因为使用了collection 和association 标签处理复杂映射时


PageHelper 分页total失效或当前页含数量不匹配问题解决方案 含原理分析

  • 前言
  • 1 第一种情况,返回list结果之后进行了二次封装
  • 2 问题原因
  • 3 解决方式
    • 方式1:不要加工了嘛,mapper 返回啥,就直接给调用方返回啥。
    • 方式2:直接让 mapper 返回最终返回给调用方的类型,不要在加工的时候生成新的 List 了。
    • 方式3:在构造 PageInfo 的时候稍加修改就可以了
  • 4 原理分析
  • 5 问题二 使用了mybatis的collection或association标签
  • 6 问题原因
  • 7 解决方案
  • 8 原理分析
  • 9 源码分析
    • 创建分页
    • 利用 MyBatis 拦截器机制
    • 最后 PageInfo 构造(这步可以明显看到出现total不准的原因)
    • 清除线程数据

1 第一种情况,返回list结果之后进行了二次封装

这个问题的流程基本如下:

  1. 设置分页参数 PageHelper.startPage(1,10)
  2. 通过一个 Mapper 查询出结果集
  3. 通过上一步的结果集构造 PageInfo
  4. 这时候,构造出的 PageInfo 是没问题的
  5. 真实情况,还要对结果集进行加工,将结果集转变了类型
  6. 这时候,通过新的结果集构造 PageInfo,分页信息就是错误的

如下简单的代码逻辑

public PageInfo<DataDetailVo> search(String keyword) {
 List<DataDetailVo> voList = new ArrayList<>();
 // 1.设置分页,第1页,10条
 PageHelper.startPage(1,10);
 // 2.查询结果集
 List<DataVo> dataVos = xxxMapper.searchDataList(keyword);

 // 3.通过上一步的结果集构造 PageInfo
 PageInfo<DataVo> pageSuccess = new PageInfo<>(dataVos);
 // 结果是对的
 log.info("pageSuccess:" + JSON.toJSONString(pageSuccess));

 // 4.真实情况,还要对结果集进行加工,将结果集转变了类型
 for (DataVo dataVo : dataVos) {
  DataDetailVo vo = new DataDetailVo();
  BeanUtils.copyProperties(dataVo, vo);
  voList.add(vo);
 }

 // 5.这时候,通过新的结果集构造 PageInfo,分页信息就是错误的
 PageInfo<DataDetailVo> pageFail = new PageInfo<>(voList);
 log.info("pageFail:" + JSON.toJSONString(pageFail));
 
 return pageFail;
}

2 问题原因

因为第5步构造 PageInfo 时使用了一个新的 List,才导致分页失效的。

3 解决方式

方式1:不要加工了嘛,mapper 返回啥,就直接给调用方返回啥。

方式2:直接让 mapper 返回最终返回给调用方的类型,不要在加工的时候生成新的 List 了。

这种也可以,但是改动可能比较大,因为有的 Mapper 层的方法是供很多其他方法调用的,Mapper 层基本上只需要返回最通用的类型。不能为了某个方法调用方,而让其他调用方也做出改变。

方式3:在构造 PageInfo 的时候稍加修改就可以了

只需要将原本构造错误的 PageInfo
.

PageInfo<DataDetailVo> pageFail = new PageInfo<>(voList);
log.info("pageFail:" + JSON.toJSONString(pageFail));

改为下面这样既可,还是用 Mapper 层返回的dataVos 集合来构造 PageInfo,只不过稍后将加工后的新的List 赋值给 PageInfo 的 list 属性即可。

PageInfo pageSuccess2 = new PageInfo<>(dataVos);
pageSuccess2.setList(voList);

4 原理分析

Mapper 查出来的是一个 List(dataVos),经过加工的也是 List(voList),怎么就一个正常,一个不正常呢,难道这两个 List 有什么不一样的吗?

我先说结论,这俩 List 确实不一样,确切的说,Mapper 查出来的那个 List 是被 PageHelper 包装后的List,再确切的说是 PageHelper 里的 Page 对象。

通过调试代码可以看出来,dataVos 就是一个披着 List 外衣的 Page 对象,你可以直接在这个对象上调用 Page 中的方法,比如 getTotal(),可以直接返回数量的。
如下调试代码:
在这里插入图片描述

也有这种样式的调试窗口:
在这里插入图片描述
而你自己加工后的集合,就真的是个单纯的 ArrayList 了,所以在使用 PageInfo 构造分页对象的时候,使用自己加工后的集合是绝对不可能获取到真实的分页参数的,比如总条数、总页数等。

5 问题二 使用了mybatis的collection或association标签

如下

public class User {
    private Long id;
    private String name;
    private List<Order> orders;  // 一对多关系
    // getter和setter方法
}

public class Order {
    private Long id;
    private String orderName;
    // getter和setter方法
}


MyBatis Mapper XML

<resultMap id="userResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
    <collection property="orders" ofType="Order">
        <id property="id" column="order_id"/>
        <result property="orderName" column="order_name"/>
    </collection>
</resultMap>

<select id="selectUsersWithOrders" resultMap="userResultMap">
    SELECT u.id AS user_id, u.name AS user_name, 
           o.id AS order_id, o.order_name 
    FROM user u 
    LEFT JOIN order o ON u.id = o.user_id;
</select>


6 问题原因

使用了collection 和association 标签处理复杂映射时,可能会导致 PageHelper 失效。这是因为 PageHelper 的分页逻辑依赖于 SQL 查询的结果集,而 MyBatis 的嵌套映射可能会影响分页的正确性。

7 解决方案

  1. 拆分查询,手动分页。对主表查询进行分页,其它的再通过关联字段查询 增加代码量
  2. 在 SQL 中使用 JOIN 查询并手动组装,一次性获取所有数据,然后通过代码组装,代码量剧增 不推荐
  3. 对需要分页的数据查询之后在结果集中使用collection 和association 标签,在这些标签里写入子查询。将子查询的结果封装进去。(每条数据对应的子查询都需要单独查询,在一些对性能要求较高的场景下,可能有性能更高的解决方案,需要根据情况使用)

如下

<resultMap id="userResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
    <collection property="orders" ofType="Order" select="findByOrderIdLogs"  column="id" >
    </collection>
</resultMap>

<select id="selectUsersWithOrders" resultMap="userResultMap">
    SELECT u.id AS user_id, u.name AS user_name
    FROM user u 
</select>


    <select id="findByOrderIdLogs" resultMap="findLogsMap">
  SELECT o.id AS order_id, o.order_name 
    FROM order o WHERE o.user_id = #{id}
    </select>

8 原理分析

PageHelper 的核心原理是通过拦截sql执行,自动在sql语句中加入limit 和offset分句,以实现分页功能。但是如果使用了嵌套查询,如多条记录对应了一个对象,但是PageHelper并不知道这个情况,还会将原来查询的数量返回,导致数量不匹配。

9 源码分析

创建分页

首先通过代码 PageHelper.startPage(1,10);设置分页参数,这个过程很简单,就是初始化 Page 对象,然后存到 ThreadLocal 中。


    /**
     * 开始分页
     *
     * @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);
        //当已经执行过orderBy的时候
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        setLocalPage(page);
        return page;
    }

ThreadLocal 是 Java 提供的一种用于保存线程内部局部变量的工具类。它为每个线程提供了独立的变量副本,每个线程只能访问自己存储的变量,互不干扰。这种设计可以避免多个线程之间共享变量时的并发问题。

在 PageHelper 中,分页的上下文需要在多个方法之间传递,但它是和当前线程强相关的。如果直接使用普通成员变量,那么在高并发情况下,多个线程可能会互相干扰。

利用 MyBatis 拦截器机制

然后就是利用了 MyBatis 的拦截器机制,拦截器主要做两件事,第一件就是在查询数据集合前先count一下,把数量查出来。第二件就是将查询出来的数据集包装成 Page 对象,当然了 Page 是继承自 ArrayList 的,要不然它也不能伪装的这么好。

在 PageHelper 源码中有 PageInterceptor.java这个拦截器,主要是里面的 intercept 方法。这里面就是实现核心逻辑的主战场。

public Object intercept(Invocation invocation) throws Throwable {
 try {
  // ...
  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);
 }
}

先判断是否需要进行分页,如果不需要,直接返回结果。也就是这行代码,你可以点进去看一下 skip 这个方法,就是获取 ThreadLocal 中的Page对象,看是不是存在,是不是有分页参数,有的话就是需要分页,没有就直接按照正常的查询走了。

if (!dialect.skip(ms, parameter, rowBounds))

如果需要分页的话,先查询一下数量。

Long count = count(executor, ms, parameter, rowBounds, null, boundSql);

然后根据分页参数,查询分页结果集。

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

之后对结果集加工并返回。

return dialect.afterPage(resultList, parameter, rowBounds);

最终加工成 Page 的方法,看到没,还是先从 ThreadLocal中拿,然后将原始结果集放进去。

public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
 Page page = getLocalPage();
 if (page == null) {
  return pageList;
 }
 page.addAll(pageList);
 if (!page.isCount()) {
  page.setTotal(-1);
 } else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
  page.setTotal(pageList.size());
 } else if (page.isOrderByOnly()) {
  page.setTotal(pageList.size());
 }
 return page;
}

最后 PageInfo 构造(这步可以明显看到出现total不准的原因)

创建分页时 PageInfo调用了super(list)
在这里插入图片描述
在父类的构造方法中,如果时page类型的调用getTotal(),如果不是,则调用size()方法

在这里插入图片描述

    @SuppressWarnings("unchecked")
    public PageSerializable(List<? extends T> list) {
        this.list = (List<T>) list;
        if(list instanceof Page){
            this.total = ((Page<?>)list).getTotal();
        } else {
            this.total = list.size();
        }
    }

清除线程数据

public static void clearPage() {
    LOCAL_PAGE.remove();  // 清除当前线程的 Page 对象
}


为什么需要手动清理 ThreadLocal?
ThreadLocal 的变量存储在当前线程中,如果线程是线程池中的线程,它会被重复使用。如果不手动清理,可能会导致内存泄漏问题,或者后续任务错误地使用到上一次分页的残留数据。

因此,PageHelper 通常会在分页逻辑结束后调用 clearPage() 方法:


http://www.kler.cn/a/407261.html

相关文章:

  • 数据结构(初阶6)---二叉树(遍历——递归的艺术)(详解)
  • Unity图形学之CubeMap立方体贴图
  • 嵌入式硬件实战基础篇(二)-稳定输出3.3V的太阳能电池-无限充放电
  • Redis原理及应用
  • MacOS通过X11转发远程运行virt-manager进行虚机分配
  • 《机器人控制器设计与编程》考试试卷**********大学2024~2025学年第(1)学期
  • 博图unified Wincc自定义控件-json自动更新导航栏
  • 「Mac玩转仓颉内测版30」基础篇10 - 区间类型详解
  • CSS3_媒体查询(十一)
  • WPF触发器
  • 组合模式和适配器模式的区别
  • C++练级计划->《海量数据处理》面试题
  • 力扣面试经典 150(上)
  • 【MATLAB源码-第221期】基于matlab的Massive-MIMO误码率随着接收天线变化仿真,对比ZF MMSE MRC三种检测算法。
  • oracle查看锁阻塞-谁阻塞了谁
  • 【SLAM文献阅读】基于概率模型的视觉SLAM动态检测与数据关联方法
  • go 结构体方法
  • Ubuntu下安装Qt
  • 工程企业需要什么样的物资管理系统?为什么需要物资管理系统?
  • LWE详细介绍
  • 网络安全的学习方向和路线是怎么样的?
  • 【AIGC】大模型面试高频考点-RAG篇
  • 深度学习:神经网络的搭建
  • Python实现随机分布式延迟PSO优化算法(RODDPSO)优化CNN回归模型项目实战
  • Android学生信息管理APP的设计与开发
  • Webpack 热更新(HMR)详解:原理与实现