RuoYi 中使用 PageUtils.startPage() 实现分页查询的完整解析

该文章已生成可运行项目,

在基于 RuoYi 框架开发过程中,我们经常需要实现分页查询功能。RuoYi 默认集成的是 MyBatis 分页插件 PageHelper,它通过拦截 SQL 查询语句,自动添加分页逻辑并封装结果,从而简化分页操作。

然而,在实际开发中,如果不理解其内部机制或处理不当,很容易导致分页信息丢失,尤其是最核心的 total(总记录数)无法返回给前端。

本文将从原理出发,结合实际代码示例,深入分析 PageHelper 的工作方式,并指出一个常见的错误用法:Service 层中使用 new ArrayList<>() 造成分页信息丢失的问题


一、PageHelper 简介与基本用法

PageHelper 是一个为 MyBatis 提供分页功能的第三方插件。它通过 ThreadLocal 存储当前线程的分页参数,并在执行下一条查询语句时动态生成带 LIMIT 的 SQL,同时生成统计总数的 SQL。

使用方式如下:

// Controller 中调用
PageUtils.startPage(); // 内部调用 PageHelper.startPage(pageNum, pageSize)
List<AccountInfo> list = accountInfoMapper.selectAccountInfoList(request);
return getDataTable(list);

其中:

  • startPage() 方法会从请求参数中提取当前页码和每页大小;
  • accountInfoMapper.selectAccountInfoList() 执行数据库查询;
  • getDataTable() 将查询结果封装成 TableDataInfo 返回给前端。

二、Mapper 接口返回类型对分页的影响

这是关键所在。

1. 返回 Page<T> 类型(推荐)

@Mapper
public interface AccountInfoMapper {
    Page<AccountInfo> selectAccountInfoList(AccountInfoRequest request);
}

此时,查询结果是一个 Page<T> 类型对象,它是 ArrayList<T> 的子类,扩展了以下属性:

  • pageNum:当前页码;
  • pageSize:每页大小;
  • total:总记录数;
  • pages:总页数;
  • hasPreviousPage / hasNextPage:是否包含上一页/下一页。

因此,你可以直接获取到这些信息,用于构建标准分页响应。

2. 返回 List<T> 类型(不推荐)

@Mapper
public interface AccountInfoMapper {
    List<AccountInfo> selectAccountInfoList(AccountInfoRequest request);
}

虽然底层确实执行了分页查询,但返回值被强转为 List<T>,导致所有的分页元数据(如 total)都被丢弃。

即使你在 Service 层遍历后重新封装为新的 List<T>,也无法再恢复分页信息。


三、解析RuoYi 是如何使用 PageUtils.startPage()

但是纵观ruoyi框架,他在mapper层返回的就是list,代码如下:
在这里插入图片描述

但是还是可以分页的,这是为啥

1、简要回答:

虽然 Mapper 返回的是 List<T>,但由于 PageHelper 的内部机制和 RuoYi 的封装设计,仍然可以获取到分页信息(如 total)。

但这并不是因为 List<T> 本身携带了分页信息,而是因为:

  • PageHelper 在执行查询前设置了分页上下文;
  • 查询后通过 ThreadLocal 缓存了分页结果;
  • 最终通过 PageInfo 或其他工具类从缓存中取出 total

2、 详细解析

1. RuoYi 中典型的 Controller 分页代码结构如下:

@GetMapping("/list")
public TableDataInfo list(ActiveDiscoveryRequest request) {
    startPage(); // 开启分页
    List<ActiveDiscovery> list = activeDiscoveryService.selectActiveDiscoveryList(request);
    return getDataTable(list);
}

其中:

  • startPage():调用 PageHelper.startPage(pageNum, pageSize)
  • selectActiveDiscoveryList():Mapper 方法返回的是 List<T>
  • getDataTable(list):构造并返回 TableDataInfo

2. PageHelper 是如何工作的?

(1)PageHelper 使用 ThreadLocal 存储分页参数

当你调用:

PageUtils.startPage();

它内部会调用:

PageHelper.startPage(pageNum, pageSize);

此时 PageHelper 将当前线程的分页参数保存到 ThreadLocal 中。

(2)执行 SQL 查询时拦截并生成分页语句

当执行下一条查询语句时,PageHelper 会:

  • 自动将 SQL 改写为带 LIMIT 的语句;
  • 同时生成一条统计总数的 SQL;
  • 执行完成后将结果返回为 List<T>
  • 但同时也会把分页信息(如 total)缓存在 ThreadLocal 中。
(3)使用 PageInfo 获取 total

RuoYi 中通常有一个 BaseController.getDataTable() 方法,类似如下:

protected TableDataInfo getDataTable(List<?> list) {
    TableDataInfo rspData = new TableDataInfo();
    rspData.setRows(list);
    rspData.setTotal(new PageInfo<>(list).getTotal());
    return rspData;
}

这里的关键是:

new PageInfo<>(list).getTotal()

这个 PageInfo 是通过反射访问 ThreadLocal 中缓存的分页信息来获取 total 的。


3、 所以你看到的流程是这样的:

startPage() → 设置 ThreadLocal 分页参数
↓
执行 Mapper 查询 → 返回 List<T>
↓
new PageInfo<>(list) → 通过 ThreadLocal 获取 total
↓
构造 TableDataInfo 并返回给前端

4、 总结:为什么返回 List 还能分页?

原因说明
PageHelper 的 ThreadLocal 缓存机制即使返回的是 List,分页信息仍被缓存
PageInfo 工具类自动读取缓存能正确获取 total、pageNum、pageSize 等信息
RuoYi 的封装设计提供了统一的 getDataTable() 方法简化分页返回

5、注意事项

虽然这种方式在 RuoYi 中可以正常工作,但有以下几点需要注意:

问题风险
多次查询干扰如果一次请求中多次调用了分页查询,ThreadLocal 中的数据可能会被覆盖或混淆
异步线程丢失上下文如果分页查询发生在子线程中,ThreadLocal 数据不会自动传递
PageInfo 依赖反射性能略低于直接使用 Page
不适用于复杂场景如需处理多个分页结果、自定义分页逻辑时,推荐使用 Page

6、推荐做法(适用于复杂业务)

如果你需要更清晰、可控的分页逻辑,建议:

@Mapper
public interface ActiveDiscoveryMapper {
    Page<ActiveDiscovery> selectActiveDiscoveryList(ActiveDiscoveryRequest request);
}

然后在 Service 层:

Page<ActiveDiscovery> page = activeDiscoveryMapper.selectActiveDiscoveryList(request);
TableDataInfo dataTable = new TableDataInfo();
dataTable.setRows(page);
dataTable.setTotal(page.getTotal());
return dataTable;

这样可以避免对 ThreadLocal 和 PageInfo 的依赖,逻辑更清晰。


7、结论一句话:

虽然 Mapper 返回的是 List,但在 RuoYi 中借助 PageHelper 的 ThreadLocal 缓存 + PageInfo 工具类,仍然可以正确获取 total 字段并实现分页功能。

这是 RuoYi 框架的一个巧妙封装设计,但也存在一定局限性,适合简单场景。对于复杂业务,建议使用 Page<T> 显式保留分页信息。

三、Service 层中的常见错误:new ArrayList<>() 导致分页信息丢失

这是本篇文章的重点部分

1、错误写法示例:

@Override
public List<AccountInfoRespone> selectAccountInfoList(AccountInfoRequest request) {
    List<AccountInfoRespone> respList = new ArrayList<>();
    List<AccountInfo> accountInfos = accountInfoMapper.selectAccountInfoList(request);

    for (AccountInfo info : accountInfos) {
        //这里对数据进行处理
        AccountInfoRespone vo = dataFormat(info);
        respList.add(vo);
    }

    return respList;
}

在这个例子中:

  • accountInfos 是由 Mapper 返回的 List<T>
  • 虽然底层确实是分页查询的结果,但由于没有保留原始的 Page<T> 对象;
  • 最终返回的是一个新的 ArrayList<>()分页信息完全丢失
  • 前端无法知道总共有多少条记录,也就无法正常显示分页控件。

2、正确做法:

应在 Service 层中保持 Page<T> 类型,并在此基础上进行 VO 转换。

@Override
public TableDataInfo selectAccountInfoList(AccountInfoRequest request) {
    Page<AccountInfo> accountInfoPage = accountInfoMapper.selectAccountInfoList(request);

    List<AccountInfoRespone> respList = new ArrayList<>();
    for (AccountInfo info : accountInfoPage) {
        AccountInfoRespone vo = dataFormat(info);
        respList.add(vo);
    }

    TableDataInfo dataTable = new TableDataInfo();
    dataTable.setRows(respList);
    dataTable.setTotal(accountInfoPage.getTotal());

    return dataTable;
}

或者使用ruoyi的框架原理,使用new PageInfo

@Override
public TableDataInfo selectAccountInfoList(AccountInfoRequest request) {
    Page<AccountInfo> accountInfoPage = accountInfoMapper.selectAccountInfoList(request);
	long total = new PageInfo(accountInfoPage).getTotal();
    List<AccountInfoRespone> respList = new ArrayList<>();
    for (AccountInfo info : accountInfoPage) {
        AccountInfoRespone vo = dataFormat(info);
        respList.add(vo);
    }

    TableDataInfo rspData = new TableDataInfo();
    rspData.setCode(HttpStatus.SUCCESS);
    rspData.setRows(respList );
    rspData.setMsg("查询成功");
    rspData.setTotal(total);

    return dataTable;
}

这样可以确保:

  • 数据转换完成;
  • 分页信息(如 total)未丢失;
  • 返回给前端的结构是完整的。

四、XML 文件中是否需要特殊设置?

不需要!

即使你的 XML 文件中 SQL 写法如下:

<select id="selectAccountInfoList" parameterType="com.example.dto.AccountInfoRequest" resultType="com.example.entity.AccountInfo">
    SELECT * FROM account_info
    <where>
        <if test="delFlag != null"> AND del_flag = #{delFlag} </if>
    </where>
</select>

只要接口方法返回的是 Page<T> 类型,PageHelper 会在运行时自动拦截该 SQL,生成两条语句:

  1. 统计总数:SELECT COUNT(*) FROM account_info WHERE ...
  2. 分页查询:SELECT * FROM account_info WHERE ... LIMIT pageNum, pageSize

因此,SQL 无需任何修改,只需关注业务逻辑即可。


五、TableDataInfo 标准返回格式定义

public class TableDataInfo {
    private long total;   // 总记录数
    private List<?> rows; // 当前页数据

    // getter/setter
}

前端可以通过这个结构准确获取当前页的数据列表以及总记录数,从而正确渲染分页组件。


六、最佳实践总结

项目推荐做法
Mapper 接口返回类型Page<T>
XML SQL 写法❌ 不需要特殊修改
Service 层处理✅ 使用 Page<T> 进行 VO 转换并封装 TableDataInfo
Controller 层✅ 只负责调用和返回
VO 转换方式✅ 使用 MapStruct 替代 BeanUtils.copyProperties
分页工具类✅ 抽取通用分页转换工具类

七、结语

PageHelper 是一个强大且易用的分页插件,但在使用过程中必须注意:

  • 不要随意丢弃 Page<T> 对象
  • 避免在 Service 层中使用 new ArrayList<>() 包装分页结果
  • 确保最终返回的 TableDataInfo 包含 total 字段

只有这样才能保证前后端协同工作的准确性,避免出现“前端分页失效”、“total 为 0”等常见问题。

如果你正在使用 RuoYi 开发后台管理系统,请务必重视分页逻辑的设计与实现,确保每一次查询都能正确携带分页信息。

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苍煜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值