MyBatis分页插件案例

wen java案例 2

本文目录导读:

MyBatis分页插件案例

  1. 目录导读
  2. 分页之痛:为什么原生MyBatis分页让人抓狂?
  3. 插件登场:PageHelper核心原理与执行流程解析
  4. 实战案例:Spring Boot集成PageHelper完整步骤
  5. 进阶技巧:多数据源分页、性能优化陷阱
  6. 常见问答:面试官最爱问的5个分页问题

目录导读

  1. 分页之痛:为什么原生MyBatis分页让人抓狂?
  2. 插件登场:PageHelper核心原理与执行流程解析
  3. 实战案例:Spring Boot集成PageHelper完整步骤(含代码)
  4. 进阶技巧:多数据源分页、动态表名、性能优化陷阱
  5. 常见问答:面试官最爱问的5个分页问题

分页之痛:为什么原生MyBatis分页让人抓狂?

在Java企业级开发中,列表分页是最基础的需求,但直接使用MyBatis原生分页时,开发者往往陷入两难:

  • 物理分页:需要手写LIMIT #{offset}, #{pageSize},每个SQL都要拼接,且无法复用。
  • 内存分页:使用RowBounds,看似简单,实则将所有数据加载到内存再截取,数据量大时直接OOM。

更棘手的是,当业务复杂到需要关联查询、子查询时,手动计算总记录数(SELECT COUNT(*))容易与主查询条件不一致,导致分页数据错乱。分页插件正是为解决这类痛点而生


插件登场:PageHelper核心原理与执行流程解析

PageHelper是目前最主流的MyBatis分页插件,其核心机制基于MyBatis的拦截器(Interceptor),它拦截Executor.query()方法,在SQL执行前动态改写:

执行流程三步走

  1. ThreadLocal存储分页参数:调用PageHelper.startPage(pageNum, pageSize)后,参数暂存于当前线程。
  2. SQL智能改写:拦截器检测到ThreadLocal中有分页参数,则解析原SQL,自动生成:
    • 分页SQL:方言适配(MySQL自动加LIMIT,Oracle自动加ROWNUM)。
    • Count SQL:自动优化为SELECT COUNT(0),去除多余ORDER BY
  3. 结果封装:将查询结果包装为PageInfo对象,包含页码、总条数、总页数等元数据。

关键优势:对业务代码零侵入,且支持强类型安全的分页对象。


实战案例:Spring Boot集成PageHelper完整步骤

场景模拟:电商后台用户管理列表,需要按创建时间倒序分页展示,并返回用户及其所属角色名称(联表查询)。

步骤1:引入依赖(Maven)

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.7</version>
</dependency>

步骤2:配置YML(关键参数)

pagehelper:
  helper-dialect: mysql   # 指定数据库方言
  reasonable: true        # 页码越界时自动回拨(如查第100页但只有5页,则查第5页)
  support-methods-arguments: true  # 支持接口参数直接传入pageNum/pageSize

步骤3:编写Mapper接口(多表联查)

public interface UserMapper {
    // 联表查询用户及角色,XML中编写SQL
    List<UserVO> selectUserWithRole(@Param("keyword") String keyword);
}

对应的XML SQL

<select id="selectUserWithRole" resultType="com.example.vo.UserVO">
    SELECT u.id, u.username, u.create_time, r.role_name
    FROM t_user u
    LEFT JOIN t_role r ON u.role_id = r.id
    <where>
        <if test="keyword != null and keyword != ''">
            AND u.username LIKE CONCAT('%', #{keyword}, '%')
        </if>
    </where>
    ORDER BY u.create_time DESC
</select>

步骤4:Service层调用(核心模式)

public PageInfo<UserVO> getUsers(int pageNum, int pageSize, String keyword) {
    // 1. 启动分页(必须在查询语句之前)
    PageHelper.startPage(pageNum, pageize);
    // 2. 执行查询(此时会被插件拦截改写)
    List<UserVO> userList = userMapper.selectUserWithRole(keyword);
    // 3. 封装分页结果
    return new PageInfo<>(userList);
}

运行效果

  • pageNum=2, pageSize=10时,自动生成:
    • Count SQL:SELECT COUNT(0) FROM t_user u LEFT JOIN t_role r ON ... WHERE u.username LIKE '%张%'
    • 数据SQL:SELECT ... LIMIT 10, 10
  • 前端可轻松从PageInfo获取总条数、总页数、是否首页/末页

进阶技巧:多数据源分页、性能优化陷阱

陷阱1:startPage后必须紧跟查询

// 错误写法
PageHelper.startPage(1, 10);
User user = new User(); // 中间掺杂其他操作
List<User> list = userMapper.selectAll(); // 分页无效
// 正确做法:startPage后直接调用Mapper方法

陷阱2:嵌套查询导致Count语句错误

若SQL中使用了UNIONFOR UPDATE,需手动指定countSuffix

PageHelper.startPage(1, 10, true).setCountSuffix("_COUNT");
// Mapper中定义 <select id="selectAll_COUNT"> SELECT COUNT(*) FROM ... </select>

陷阱3:多数据源下方言冲突

若存在MySQL和Oracle双数据源,需在@DataSource切换前强制指定方言:

PageHelper.getLocalPage().setDialect(new MySqlDialect());
// 或者全局关闭自动识别,yml中配置 helper-dialect: mysql

性能优化最佳实践

  • 严禁大字段分页:若查询列包含TEXTBLOB,建议先查主键分页后再回表。
  • Count查询优化:PageHelper默认生成的count语句会去除ORDER BY,但对复杂JOIN,可手动覆盖count SQL以提升速度。

常见问答:面试官最爱问的5个分页问题

Q1:PageHelper的原理是否会破坏MyBatis一级缓存? A:不会,PageHelper只改写SQL,不涉及缓存,但需注意startPage后查询完,自动清除ThreadLocal中的分页参数,避免线程池复用导致数据错乱。

Q2:为什么我用PageInfo获取total时总是多了一次count查询? A:这是正常现象,PageHelper总是执行一次count查询来获取总数,若追求极致性能,可通过PageHelper.startPage(1, 10, false)禁用count查询,此时total为0。

Q3:分页插件能处理LEFT JOIN产生的重复记录吗? A:不能,这是SQL本身的问题,需通过DISTINCTGROUP BY消除重复,分页插件只是基于你提供的最终结果集进行物理分页。

Q4:在Spring事务中,分页查询回滚会怎样? A:完全安全,分页插件只操作SQL语句生成,不参与事务管理,若事务回滚,查询结果也会一并回滚,无副作用。

Q5:分页插件在大数据量(百万级)下性能如何? A:物理分页性能远优于内存分页,但LIMIT 100000, 10这种深分页依然很慢,推荐游标分页(基于WHERE id > #{lastId})或者用子查询优化深分页。

抱歉,评论功能暂时关闭!