乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

分页插件

导学

本节学习目标:

  • 理解RowBounds逻辑分页的原理与局限性
  • 掌握物理分页插件的核心实现机制(拦截器改写SQL)
  • 通过SQL日志对比两种分页方式的执行差异
  • 能够根据数据量选择合适的分页策略

定义

分页是数据查询中最常见的需求之一。MyBatis提供两种分页方案,解决不同规模数据集的结果集截取痛点:

  • RowBounds逻辑分页:在内存中对全量结果做截取,实现简单但性能差
  • 物理分页插件:在SQL层面添加LIMIT子句,只查询所需页数据,性能高但需插件支持

选择合适的分页方式,直接决定了大数据量查询的响应速度与内存占用。

核心原理

逻辑分页 vs 物理分页流程对比

RowBounds逻辑分页机制

RowBounds是MyBatis内置的分页对象,包含两个属性:

  • offset:跳过的记录数
  • limit:返回的最大记录数

使用方式:

RowBounds rowBounds = new RowBounds(0, 2);  // 第1页,每页2条
List<Student> list = sqlSession.selectList("selectStudents", null, rowBounds);

本质:MyBatis在ResultHandler中判断已处理行数,小于offset则跳过,达到limit则停止遍历。数据库仍然执行全表查询,所有数据都经过网络传输和JDBC解析。

物理分页插件原理(以PageHelper为例)

物理分页插件基于MyBatis的**拦截器(Interceptor)**机制,核心流程:

  1. 拦截:通过@Intercepts注解拦截Executor.query()方法
  2. 提取:从ThreadLocal或方法参数中获取分页参数(页码、页大小)
  3. 解析:使用JSqlParser等工具解析原始SQL的SELECT语句
  4. 改写:在SQL末尾追加数据库方言对应的LIMIT子句(MySQL用LIMIT offset, limit,Oracle用ROWNUM等)
  5. 执行:将改写后的SQL交给数据库执行
  6. 统计:可选地执行COUNT(*)查询,封装PageInfo返回总页数、总记录数等元数据

完整示例

场景说明

乐途公司人事系统需要分页展示员工列表。本节对比RowBounds和PageHelper两种方案,观察SQL输出差异与内存行为。

操作前的数据库表结构及初始数据

CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    age INT,
    major VARCHAR(20),
    score DECIMAL(5,2)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);

当前数据状态:

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00

完整代码与配置

pom.xml依赖

<dependencies>
    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.13</version>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <!-- PageHelper分页插件 -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.3.2</version>
    </dependency>
</dependencies>

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="logImpl" value="SLF4J"/>
    </settings>
    
    <!-- 注册PageHelper插件 -->
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 配置MySQL方言 -->
            <property name="helperDialect" value="mysql"/>
            <!-- 分页合理化,页码<1时查第一页,页码>总页数时查最后一页 -->
            <property name="reasonable" value="true"/>
        </plugin>
    </plugins>
    
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/fly_db?useSSL=false&amp;serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    
    <mappers>
        <mapper resource="com/fly/mapper/StudentMapper.xml"/>
    </mappers>
</configuration>

StudentMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fly.mapper.StudentMapper">
    
    <!-- 无分页参数的普通查询 -->
    <select id="selectAll" resultType="com.fly.entity.Student">
        SELECT id, name, age, major, score FROM student
    </select>
    
    <!-- 按专业查询 -->
    <select id="selectByMajor" resultType="com.fly.entity.Student">
        SELECT id, name, age, major, score FROM student WHERE major = #{major}
    </select>
    
</mapper>

Student.java

package com.fly.entity;

public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String major;
    private Double score;
    
    // Getter与Setter省略
}

PageCompareDemo.java

package com.fly.demo;

import com.fly.entity.Student;
import com.fly.mapper.StudentMapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;
import java.util.List;

public class PageCompareDemo {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        
        System.out.println("========== 方式一:RowBounds 逻辑分页 ==========");
        testRowBounds(sqlSessionFactory);
        
        System.out.println("\n========== 方式二:PageHelper 物理分页 ==========");
        testPageHelper(sqlSessionFactory);
    }
    
    static void testRowBounds(SqlSessionFactory sqlSessionFactory) {
        try (SqlSession session = sqlSessionFactory.openSession()) {
            StudentMapper mapper = session.getMapper(StudentMapper.class);
            
            // RowBounds(offset=0, limit=2):第1页,每页2条
            RowBounds rowBounds = new RowBounds(0, 2);
            List<Student> list = session.selectList(
                "com.fly.mapper.StudentMapper.selectAll", 
                null, 
                rowBounds
            );
            
            System.out.println("当前页数据:");
            for (Student s : list) {
                System.out.println("  " + s.getId() + " | " + s.getName() + " | " + s.getMajor());
            }
            System.out.println("返回条数:" + list.size());
            // RowBounds不返回总条数,需额外查询
        }
    }
    
    static void testPageHelper(SqlSessionFactory sqlSessionFactory) {
        try (SqlSession session = sqlSessionFactory.openSession()) {
            StudentMapper mapper = session.getMapper(StudentMapper.class);
            
            // PageHelper物理分页:第1页,每页2条
            PageHelper.startPage(1, 2);
            List<Student> list = mapper.selectAll();
            
            // 封装分页元数据
            PageInfo<Student> pageInfo = new PageInfo<>(list);
            
            System.out.println("当前页数据:");
            for (Student s : list) {
                System.out.println("  " + s.getId() + " | " + s.getName() + " | " + s.getMajor());
            }
            System.out.println("返回条数:" + list.size());
            System.out.println("总记录数:" + pageInfo.getTotal());
            System.out.println("总页数:" + pageInfo.getPages());
            System.out.println("当前页码:" + pageInfo.getPageNum());
            System.out.println("每页大小:" + pageInfo.getPageSize());
        }
    }
}

实际执行结果

控制台SQL日志输出:

========== 方式一:RowBounds 逻辑分页 ==========
[com.fly.mapper.StudentMapper.selectAll] - ==>  Preparing: SELECT id, name, age, major, score FROM student
[com.fly.mapper.StudentMapper.selectAll] - ==> Parameters: 
[com.fly.mapper.StudentMapper.selectAll] - <==      Total: 5
当前页数据:
  1 | 大翔 | 计算机科学
  2 | 白歌 | 软件工程
返回条数:2

========== 方式二:PageHelper 物理分页 ==========
[com.fly.mapper.StudentMapper.selectAll_COUNT] - ==>  Preparing: SELECT count(0) FROM student
[com.fly.mapper.StudentMapper.selectAll_COUNT] - ==> Parameters: 
[com.fly.mapper.StudentMapper.selectAll_COUNT] - <==      Total: 1
[com.fly.mapper.StudentMapper.selectAll] - ==>  Preparing: SELECT id, name, age, major, score FROM student LIMIT ?
[com.fly.mapper.StudentMapper.selectAll] - ==> Parameters: 2(Integer)
[com.fly.mapper.StudentMapper.selectAll] - <==      Total: 2
当前页数据:
  1 | 大翔 | 计算机科学
  2 | 白歌 | 软件工程
返回条数:2
总记录数:5
总页数:3
当前页码:1
每页大小:2

分析

对比维度RowBounds逻辑分页PageHelper物理分页
执行的SQLSELECT * FROM studentSELECT ... LIMIT 2 + SELECT count(0)
数据库返回行数5条(全表)2条(仅当前页)
网络传输量全量数据当前页数据
内存占用全量数据进入内存后截取仅当前页数据
总记录数获取需额外手动查询自动执行COUNT,封装到PageInfo
适用数据量小数据量(<1000条)任意数据量,大数据量必选
代码侵入性需传入RowBounds参数ThreadLocal传参,Mapper接口无感知
  1. RowBounds:数据库执行无LIMIT的全表查询,返回全部5条记录,MyBatis在内存中只取前2条。当日志显示Total: 5时,说明JDBC结果集确实遍历了5行
  2. PageHelper:自动改写SQL为LIMIT 2,数据库仅返回2条;同时自动发起COUNT查询获取总记录数5,计算总页数3。当日志显示Total: 2时,说明数据库只返回了当前页数据

关键结论:数据量较小时,RowBounds简单可用;数据量超过千级或万级时,必须使用物理分页,否则全表查询会导致数据库压力和内存溢出。

易错场景与常见误区

误区正解
RowBounds是物理分页RowBounds是逻辑分页,数据库仍执行全表查询,只是MyBatis在内存中跳过前offset条
PageHelper对任何查询都自动分页PageHelper通过ThreadLocal传递分页参数,必须在查询前一行调用startPage,且只对紧接着的第一个查询生效
物理分页插件返回的List就是全部数据物理分页返回的List只包含当前页数据,总记录数需通过PageInfo获取
RowBounds不需要数据库支持RowBounds确实不依赖数据库方言,但代价是性能差;这不是优势,是妥协
一个线程中多次调用startPage会叠加生效每次startPage只影响下一个查询,之后自动清理ThreadLocal;连续调用会覆盖

反例:PageHelper位置放错导致分页失效

// 反例:startPage放在查询之后,分页不生效
List<Student> list = mapper.selectAll();  // 先查询
PageHelper.startPage(1, 2);  // 错误!startPage必须在查询之前
// 此时list包含全部5条记录,没有分页

// 正解
PageHelper.startPage(1, 2);  // 先设置分页参数
List<Student> list = mapper.selectAll();  // 再查询,SQL会被改写为LIMIT 2

反例:RowBounds用于大数据量查询

// 反例:10万条数据用RowBounds
RowBounds rowBounds = new RowBounds(99900, 100);
List<Student> list = session.selectList("selectAll", null, rowBounds);
// 数据库返回10万条,网络传输和内存占用爆炸,可能导致OOM

正解:超过1000条的数据分页,必须使用物理分页插件。

面试考点

Q1:MyBatis分页方式有哪些?RowBounds是物理分页还是逻辑分页?

两种:RowBounds逻辑分页和物理分页插件(如PageHelper)。RowBounds是逻辑分页,它在内存中对全量结果做截取,数据库仍执行全表查询。物理分页插件通过拦截器改写SQL,在数据库层面只查询当前页数据。

Q2:PageHelper的实现原理是什么?

PageHelper基于MyBatis插件机制(Interceptor),拦截Executor.query()方法。在查询执行前,从ThreadLocal获取分页参数,使用JSqlParser解析并改写原始SQL,追加对应数据库方言的LIMIT子句。同时自动发起COUNT查询,将总记录数、总页数等封装到PageInfo中返回。

Q3:RowBounds在什么场景下可以使用?

仅适用于数据量极小(通常几百条以内)且 simplicity 优先的场景。优点是零依赖、零配置、不依赖数据库方言;缺点是数据库压力和网络开销与数据量成正比,大数据量下会导致严重的性能问题和内存风险。

Q4:BATCH执行器和物理分页插件能一起使用吗?

可以,但需注意BATCH模式仅对insert/update/delete有效,分页针对的是select查询,两者作用于不同类型的语句,通常不会直接冲突。但若在BATCH Session中执行分页查询,行为可能不可预期,建议批量写和分页读使用不同的SqlSession。

小结

分页是ORM框架的必修课。RowBounds以简单性换取性能,适合小数据量的快速开发;物理分页插件以拦截器改写SQL的方式,在数据库层面精准控制返回数据量,是生产环境的标准选择。理解两者的SQL输出差异、内存行为差异和适用边界,才能在项目中做出正确决策。

下一章引子

至此,MyBatis缓存与性能优化的核心内容已完整覆盖:从SqlSession级的一级缓存,到Mapper级的二级缓存及其精细配置,再到自定义缓存对接企业级中间件,以及Executor执行器类型和分页插件的选型。掌握这些机制,你就能在实际项目中精准定位性能瓶颈,选择最合适的优化策略。下一章将进入MyBatis的插件与扩展机制,学习如何编写自己的拦截器,在SQL执行的关键节点植入自定义逻辑。

上一页
Executor执行器类型