跳到主要内容

分页查询

分页查询是 Web 应用中最常见的功能之一。当数据量较大时,一次性查询所有数据不仅消耗内存,还会影响用户体验。本章将介绍 MyBatis 中实现分页查询的几种方式。

分页查询概述

为什么需要分页?

假设一个用户表有 100 万条记录,如果一次性查询所有数据:

  • 内存消耗大:服务器需要一次性加载所有数据到内存
  • 响应时间长:查询和传输大量数据耗时较长
  • 用户体验差:用户不需要也不可能一次查看所有数据

分页查询只获取当前页需要的数据,显著提高了性能和用户体验。

分页的两种方式

方式说明优点缺点
内存分页先查询所有数据,在内存中截取实现简单性能差,内存消耗大
物理分页数据库层面只返回需要的行性能好,内存占用小需要编写分页 SQL

推荐使用物理分页,MyBatis 可以通过以下方式实现:

  1. 手动编写分页 SQL(LIMIT/OFFSET)
  2. 使用 PageHelper 分页插件(推荐)
  3. 自定义拦截器实现

手动分页

MySQL 分页

MySQL 使用 LIMITOFFSET 实现分页:

-- 语法
SELECT * FROM table_name LIMIT #{pageSize} OFFSET #{offset}

-- 或者
SELECT * FROM table_name LIMIT #{offset}, #{pageSize}

计算公式

offset=(pageNum1)×pageSizeoffset = (pageNum - 1) \times pageSize

其中:

  • pageNum:当前页码(从 1 开始)
  • pageSize:每页记录数
  • offset:偏移量(从 0 开始)

示例:手动分页实现

Mapper 接口

public interface UserMapper {

/**
* 分页查询用户列表
* @param offset 偏移量
* @param pageSize 每页记录数
* @return 用户列表
*/
List<User> selectByPage(@Param("offset") int offset,
@Param("pageSize") int pageSize);

/**
* 查询总记录数
* @return 总记录数
*/
long count();
}

XML 映射文件

<select id="selectByPage" resultType="User">
SELECT * FROM user
ORDER BY id DESC
LIMIT #{pageSize} OFFSET #{offset}
</select>

<select id="count" resultType="long">
SELECT COUNT(*) FROM user
</select>

Service 层

@Service
public class UserService {

@Autowired
private UserMapper userMapper;

/**
* 分页查询
* @param pageNum 当前页码(从 1 开始)
* @param pageSize 每页记录数
* @return 分页结果
*/
public PageResult<User> findByPage(int pageNum, int pageSize) {
// 计算偏移量
int offset = (pageNum - 1) * pageSize;

// 查询当前页数据
List<User> list = userMapper.selectByPage(offset, pageSize);

// 查询总记录数
long total = userMapper.count();

// 计算总页数
int pages = (int) Math.ceil((double) total / pageSize);

return new PageResult<>(list, total, pages, pageNum, pageSize);
}
}

/**
* 分页结果封装类
*/
@Data
@AllArgsConstructor
public class PageResult<T> {
private List<T> list; // 当前页数据
private long total; // 总记录数
private int pages; // 总页数
private int pageNum; // 当前页码
private int pageSize; // 每页记录数
}

Controller 层

@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserService userService;

@GetMapping
public PageResult<User> list(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
return userService.findByPage(pageNum, pageSize);
}
}

手动分页的优缺点

优点

  • 不需要额外依赖
  • 可以精确控制分页逻辑

缺点

  • 每个查询都需要写两个方法(分页查询 + 总数查询)
  • 需要手动计算偏移量和总页数
  • 不同数据库的分页语法不同,移植性差

PageHelper 分页插件

PageHelper 是 MyBatis 最流行的分页插件,它通过拦截器自动为查询添加分页语句,使用非常方便。

添加依赖

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

PageHelper 依赖 JSqlParser 解析 SQL:

<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.6</version>
</dependency>

配置插件

方式一:mybatis-config.xml 配置

<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 数据库方言(可选,插件会自动检测) -->
<property name="helperDialect" value="mysql"/>
<!-- 分页合理化:pageNum <= 0 时查询第一页,pageNum > pages 时查询最后一页 -->
<property name="reasonable" value="true"/>
<!-- 支持通过 Mapper 接口参数传递分页参数 -->
<property name="supportMethodsArguments" value="true"/>
<!-- 参数名配置 -->
<property name="params" value="count=countSql"/>
</plugin>
</plugins>

方式二:Spring 配置

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>

<!-- 配置插件 -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<value>
helperDialect=mysql
reasonable=true
</value>
</property>
</bean>
</array>
</property>
</bean>

方式三:Spring Boot 配置

使用 pagehelper-spring-boot-starter

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

application.yml 配置:

pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql

基本使用

PageHelper 的核心方法是 PageHelper.startPage(),它会在当前线程中设置分页参数,接下来的第一个查询会自动应用分页。

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;

@Service
public class UserService {

@Autowired
private UserMapper userMapper;

/**
* 基本分页查询
*/
public PageInfo<User> findByPage(int pageNum, int pageSize) {
// 1. 设置分页参数(必须写在查询之前)
PageHelper.startPage(pageNum, pageSize);

// 2. 执行查询(会自动添加分页语句)
List<User> list = userMapper.selectAll();

// 3. 封装分页信息
return new PageInfo<>(list);
}

/**
* 带排序的分页查询
*/
public PageInfo<User> findByPageWithOrder(int pageNum, int pageSize) {
// 设置分页参数,带排序
PageHelper.startPage(pageNum, pageSize, "id desc");

List<User> list = userMapper.selectAll();
return new PageInfo<>(list);
}
}

PageHelper.startPage() 参数说明

// 基本用法
PageHelper.startPage(int pageNum, int pageSize)

// 带排序
PageHelper.startPage(int pageNum, int pageSize, String orderBy)

// 使用 Page 对象设置更多参数
Page page = PageHelper.startPage(1, 10);
page.setOrderBy("id desc");
page.setCount(true); // 是否查询总数
参数类型说明
pageNumint当前页码,从 1 开始
pageSizeint每页记录数
orderByString排序字段,如 "id desc, create_time asc"
countboolean是否查询总数,默认 true
reasonableboolean分页合理化

PageInfo 类详解

PageInfo 是 PageHelper 提供的分页信息封装类,包含丰富的分页属性:

// 创建 PageInfo
PageInfo<User> pageInfo = new PageInfo<>(list);

// 或者指定导航页码数
PageInfo<User> pageInfo = new PageInfo<>(list, 5); // 导航栏显示 5 页

PageInfo 属性说明

public class PageInfo<T> {
private int pageNum; // 当前页码
private int pageSize; // 每页记录数
private int size; // 当前页的实际记录数
private int startRow; // 当前页第一条记录的行号
private int endRow; // 当前页最后一条记录的行号
private long total; // 总记录数
private int pages; // 总页数
private List<T> list; // 当前页数据
private int prePage; // 上一页页码
private int nextPage; // 下一页页码
private boolean isFirstPage; // 是否第一页
private boolean isLastPage; // 是否最后一页
private boolean hasPreviousPage; // 是否有上一页
private boolean hasNextPage; // 是否有下一页
private int navigatePages; // 导航栏页码数
private int[] navigatepageNums; // 导航栏页码数组
private int navigateFirstPage; // 导航栏第一页
private int navigateLastPage; // 导航栏最后一页
}

实际使用示例

@GetMapping("/list")
public Result list(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {

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

// 返回分页信息
Map<String, Object> result = new HashMap<>();
result.put("list", pageInfo.getList());
result.put("total", pageInfo.getTotal());
result.put("pages", pageInfo.getPages());
result.put("pageNum", pageInfo.getPageNum());
result.put("pageSize", pageInfo.getPageSize());
result.put("hasNextPage", pageInfo.isHasNextPage());

return Result.success(result);
}

分页参数传递方式

方式一:直接调用 startPage

public PageInfo<User> findByPage(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> list = userMapper.selectAll();
return new PageInfo<>(list);
}

方式二:使用参数对象

// 分页参数对象
@Data
public class PageQuery {
private Integer pageNum = 1;
private Integer pageSize = 10;
}

// Mapper 接口
List<User> selectByQuery(PageQuery query);

// Service
public PageInfo<User> findByPage(PageQuery query) {
// 启用 supportMethodsArguments 后,会自动从参数对象中获取分页参数
// 参数名必须为 pageNum 和 pageSize
PageHelper.startPage(query.getPageNum(), query.getPageSize());
List<User> list = userMapper.selectByQuery(query);
return new PageInfo<>(list);
}

方式三:Mapper 参数传递(需配置)

配置 supportMethodsArguments=true 后,可以直接在 Mapper 方法参数中传递分页参数:

// Mapper 接口
List<User> selectAll(@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);

// 直接调用即可,无需调用 startPage
// 但这种方式需要正确配置 params 属性

完整示例

实体类

@Data
public class User {
private Long id;
private String username;
private String email;
private Integer status;
private LocalDateTime createTime;
}

查询条件类

@Data
public class UserQuery {
private Integer pageNum = 1;
private Integer pageSize = 10;
private String username; // 用户名(模糊查询)
private Integer status; // 状态
}

Mapper 接口

public interface UserMapper {

List<User> selectByCondition(UserQuery query);
}

XML 映射文件

<select id="selectByCondition" resultType="User">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY id DESC
</select>

Service 层

@Service
public class UserService {

@Autowired
private UserMapper userMapper;

public PageInfo<User> findByCondition(UserQuery query) {
// 设置分页参数
PageHelper.startPage(query.getPageNum(), query.getPageSize());

// 执行查询
List<User> list = userMapper.selectByCondition(query);

// 返回分页信息
return new PageInfo<>(list);
}
}

Controller 层

@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserService userService;

@GetMapping
public Result<PageInfo<User>> list(UserQuery query) {
PageInfo<User> pageInfo = userService.findByCondition(query);
return Result.success(pageInfo);
}
}

返回结果示例

{
"code": 200,
"message": "success",
"data": {
"total": 100,
"list": [
{
"id": 1,
"username": "zhangsan",
"email": "[email protected]",
"status": 1,
"createTime": "2024-01-01 10:00:00"
}
],
"pageNum": 1,
"pageSize": 10,
"pages": 10,
"size": 10,
"prePage": 0,
"nextPage": 2,
"isFirstPage": true,
"isLastPage": false,
"hasPreviousPage": false,
"hasNextPage": true,
"navigatePages": 8,
"navigatepageNums": [1, 2, 3, 4, 5, 6, 7, 8]
}
}

PageHelper 配置参数详解

常用配置参数

参数说明默认值
helperDialect数据库方言自动检测
reasonable分页合理化false
supportMethodsArguments支持参数传递false
params参数名映射count=countSql
autoRuntimeDialect自动检测方言true
closeConn关闭连接true
pageSizeZeropageSize=0 查询全部false

helperDialect 数据库方言

PageHelper 支持多种数据库:

方言数据库
mysqlMySQL、MariaDB、SQLite
oracleOracle
postgresqlPostgreSQL
sqlserverSQL Server
db2DB2
dm达梦数据库
kingbase人大金仓

reasonable 分页合理化

reasonable=true 时:

  • pageNum <= 0 时查询第一页
  • pageNum > pages 时查询最后一页
// reasonable = false(默认)
PageHelper.startPage(0, 10); // offset = -10,SQL 会报错或返回空结果

// reasonable = true
PageHelper.startPage(0, 10); // 自动改为 pageNum = 1
PageHelper.startPage(100, 10); // 假设只有 5 页,自动改为 pageNum = 5

分页查询优化

1. 避免大偏移量

当 offset 很大时,MySQL 的 LIMIT 性能会下降:

-- 偏移量 100 万,效率很低
SELECT * FROM user LIMIT 1000000, 10

优化方案一:使用 WHERE 条件

-- 如果 id 是连续的
SELECT * FROM user WHERE id > #{lastId} LIMIT 10

优化方案二:使用子查询

SELECT * FROM user 
WHERE id >= (SELECT id FROM user ORDER BY id LIMIT 1000000, 1)
LIMIT 10

2. 避免查询总数

对于大数据量的场景,查询总数(COUNT)可能很慢。如果不需要显示总页数,可以跳过:

// 不查询总数
PageHelper.startPage(pageNum, pageSize, false);
List<User> list = userMapper.selectAll();

// 返回的是 Page 对象,而不是 PageInfo
Page<User> page = (Page<User>) list;

3. 索引优化

确保分页查询的字段有索引:

-- 为排序字段创建索引
CREATE INDEX idx_create_time ON user(create_time);

-- 为查询条件创建索引
CREATE INDEX idx_status ON user(status);

不同数据库的分页语法

数据库分页语法
MySQLSELECT * FROM table LIMIT #{pageSize} OFFSET #{offset}
OracleSELECT * FROM (SELECT a.*, ROWNUM rn FROM table a WHERE ROWNUM <= #{end}) WHERE rn > #{start}
PostgreSQLSELECT * FROM table LIMIT #{pageSize} OFFSET #{offset}
SQL ServerSELECT * FROM table ORDER BY id OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY

PageHelper 会根据配置的数据库方言自动生成对应的分页 SQL。

常见问题

1. 分页不生效

问题:调用了 startPage 但没有分页效果

原因

  • startPage 和查询之间有其他查询
  • 使用了多个数据源

解决

// 错误:中间有其他查询
PageHelper.startPage(1, 10);
int count = userMapper.count(); // 这个查询会应用分页
List<User> list = userMapper.selectAll(); // 这个查询不会分页

// 正确:startPage 后紧跟查询
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectAll(); // 这个查询会分页

2. 总数统计不准确

问题:total 数量不对

原因:SQL 中使用了 GROUP BY 或 DISTINCT

解决:使用子查询

<!-- 方式一:子查询 -->
<select id="selectByCondition" resultType="User">
SELECT * FROM (
SELECT DISTINCT user_id FROM orders WHERE status = 1
) t
</select>

<!-- 方式二:手动指定 count 查询 -->
<select id="selectByCondition" resultType="User">
SELECT DISTINCT user_id FROM orders WHERE status = 1
</select>

<select id="countByCondition" resultType="long">
SELECT COUNT(DISTINCT user_id) FROM orders WHERE status = 1
</select>

3. 分页参数丢失

问题:在 Service 层调用其他方法时分页参数丢失

原因:PageHelper 使用 ThreadLocal 存储分页参数,方法调用结束后会被清除

解决

// 方式一:在同一线程中处理
public PageInfo<User> findByPage(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> list = userMapper.selectAll(); // 必须在这里执行
return new PageInfo<>(list);
}

// 方式二:使用 Page 对象
public Page<User> findByPage(int pageNum, int pageSize) {
Page<User> page = PageHelper.startPage(pageNum, pageSize).doSelectPage(
() -> userMapper.selectAll()
);
return page;
}

小结

本章介绍了 MyBatis 分页查询的实现方式:

  • 手动分页:使用 LIMIT/OFFSET,适合简单场景
  • PageHelper 插件:最方便的分页方式,推荐使用

PageHelper 的核心要点:

  1. 在查询前调用 PageHelper.startPage()
  2. 查询结果用 PageInfo 封装
  3. 合理配置参数(reasonable、helperDialect 等)

下一章将介绍缓存机制。