MyBatis PageHelper方法详解与剖析

一、引言

在使用 MyBatis 进行数据访问时,分页查询是最常见的需求之一。PageHelper 是一个基于 MyBatis 的分页插件,通过拦截 Executor 执行的 SQL,自动在原始 SQL 上拼接分页语句,从而实现的分页功能。本文将从功能用法、核心原理以及源码剖析、实际运用几个方面,介绍 PageHelper

二、主要功能与用法

2.1 引入依赖

<dependency>
  <groupId>com.github.pagehelper</groupId>
  <artifactId>pagehelper</artifactId>
  <version>5.4.0</version>
</dependency>

2.2 简单的分页

PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);

// 返回给前端:pageInfo.getList(), pageInfo.getTotal(), pageInfo.getPages()...
  • pageNum:当前页码,从 1 开始
  • pageSize:每页记录数
  • 返回结果:通过 PageInfo 包装,包含列表、总记录数、总页数、当前页码等

2.3 排序

PageHelper.startPage(pageNum, pageSize)
          .setOrderBy("create_time desc, id asc");
List<Order> orders = orderMapper.selectByUserId(userId);

这里调用 setOrderBy可以支持多字段、多方向排序

2.4 总数的查询

PageHelper.startPage(pageNum, pageSize, false);
List<Product> products = productMapper.selectByCategory(catId);
PageInfo<Product> pageInfo = new PageInfo<>(products);

 startPage的第三个参数 表示是否执行 SELECT COUNT(*)。设置为 false 时,PageHelper 只拼接分页语句,不执行总数查询。

2.5 物理分页与逻辑分页

物理分页​​指在​​数据库层面​​直接实现分页操作,通过修改SQL语句,仅查询当前页所需的数据。这种方式通过数据库自身机制限制返回的数据量,​​减少网络传输和应用内存消耗​​,适合大数据量场景。

逻辑分页意思则是:逻辑上(前端上、表面上)实现了分页,实际还是执行了整个SQL语句

坑:不能混用 RowBounds 和 startPage!

(一)RowBounds(MyBatis原生分页)和 startPage(PageHelper物理分页)底层均依赖MyBatis的​插件拦截机制​​,混用时可能多次修改SQL,导致分页参数重复添加(如LIMIT被多次拼接),最终查询结果异常。

(二)startPage优先级高于RowBounds。若同时调用:

  • startPage会触发物理分页,生成LIMIT语句。
  • RowBounds会尝试在内存中二次分页,但此时SQL已限制数据量,可能导致分页结果错误。

物理分页的正确姿势:

//-------1.startPage(推荐)​-------------
// 物理分页:查询第2页,每页10条
PageHelper.startPage(2, 10);
List<User> users = userMapper.selectAll();



//----------2.​RowBounds----------------
//配置支持
pagehelper.support-methods-arguments=true
pagehelper.params=pageNum=pageNumKey;pageSize=pageSizeKey


//Mapper接口
List<User> selectAllWithRowBounds(RowBounds rowBounds);

// 物理分页:offset=10, limit=10(对应第2页)
List<User> users = userMapper.selectAllWithRowBounds(new RowBounds(10, 10));

example中都实现了:SELECT * FROM users LIMIT 10,10  

三、源码剖析

PageHelper 核心在于一个 MyBatis 拦截器 PageInterceptor,注册为插件后拦截 Executor.query 方法:

public class PageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取方法参数:MappedStatement、参数对象、RowBounds、ResultHandler
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];

        // 2. 判断是否需要分页:检查 ThreadLocal 中是否有分页上下文
        Page<?> page = SqlUtil.getLocalPage();
        if (page != null) {
            // 3. 清理 ThreadLocal,避免后续影响
            SqlUtil.removeLocalPage();

            // 4. 执行 count 查询(可选)
            if (page.isCount()) {
                long total = count(ms, parameter);
                page.setTotal(total);
            }

            // 5. 拼接分页 SQL:在原始 SQL 上添加 LIMIT/OFFSET
            BoundSql boundSql = ms.getBoundSql(parameter);
            String pageSql = dialect.getPageSql(boundSql.getSql(), page.getStartRow(), page.getPageSize());
            // 6. 创建新的 MappedStatement,使用 pageSql
            MappedStatement pageMs = SqlUtil.newMappedStatement(ms, boundSql, pageSql);
            args[0] = pageMs;
            args[2] = RowBounds.DEFAULT;
        }
        // 7. 执行查询
        return invocation.proceed();
    }
}
  • ThreadLocal 存储PageHelper.startPage 会将分页参数封装到 Page 对象并放入 SqlUtillocalPage 中。

  • SQL 重写:通过数据库方言 Dialect(如 MysqlDialect)生成带 LIMIT 的分页 SQL。

  • MappedStatement 克隆:在原有 MappedStatement 基础上,构造一个新的包含分页 SQL 的实例。

  • 总数查询:执行一次 SELECT COUNT(*),通过 MappedStatementSqlSource 修改为 count 语句。

四、pageHelper配置

pagehelper:
  helperDialect: mysql  # 数据库方言,可选 mysql, postgresql, oracle...
  reasonable: true     # 页码 < 1 时,自动查询第一页;页码 > 最大页时,查询最后一页
  supportMethodsArguments: false # 支持从 Mapper 方法参数中读取分页参数
  params: count=countSql # 自定义 count 查询的参数名

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值