在 Java 后端开发中,分页查询是高频需求 ------ 无论是用户列表、订单记录还是数据分析,都需要通过分页来限制数据量,提升接口响应速度和用户体验。传统的 MyBatis 实现分页需要手动编写LIMIT
语句、计算总条数,代码冗余且容易出错。而 MyBatis-Plus(简称 MP)的分页功能,能让这一切变得简单:一行配置、一行代码,就能完成分页查询,甚至支持复杂条件、自定义 SQL、联表查询等场景。
本文将从基础用法到高级技巧,全方位解析 MyBatis-Plus 的分页功能,结合 20 + 实战案例,让你彻底掌握如何用 MP 写出简洁高效的分页代码,从此告别分页查询的繁琐操作。
一、为什么说传统分页是 "体力活"?
在介绍 MyBatis-Plus 的分页之前,我们先回顾一下传统 MyBatis 实现分页的流程。假设要查询 "第 2 页的用户列表,每页 10 条,按创建时间倒序",你需要做这些事:
- 编写 Mapper 接口:定义两个方法,一个查列表,一个查总条数;
- 编写 XML 映射 :手动拼接
LIMIT
语句(LIMIT 10 OFFSET 10
),总条数查询用COUNT(*)
; - 处理分页参数 :在 Service 层计算
offset = (pageNum - 1) * pageSize
; - 封装分页结果:将 "列表数据 + 总条数 + 页码 + 页大小" 封装到分页对象中。
传统 MyBatis 分页代码示例:
java
// 1. Mapper接口
public interface UserMapper {
// 查询分页列表
List<User> selectUserPage(@Param("offset") int offset, @Param("pageSize") int pageSize);
// 查询总条数
int selectUserCount();
}
// 2. XML映射文件
<select id="selectUserPage" resultType="User">
SELECT * FROM user ORDER BY create_time DESC LIMIT #{offset}, #{pageSize}
</select>
<select id="selectUserCount" resultType="int">
SELECT COUNT(*) FROM user
</select>
// 3. Service层调用
public PageResult<User> getUserPage(int pageNum, int pageSize) {
int offset = (pageNum - 1) * pageSize;
List<User> records = userMapper.selectUserPage(offset, pageSize);
int total = userMapper.selectUserCount();
return new PageResult<>(records, total, pageNum, pageSize);
}
这套流程看似简单,但存在诸多问题:
- 代码冗余:每个分页查询都要写两个方法(列表 + 总数),重复劳动;
- 容易出错 :
offset
计算错误(如pageNum=0
导致负数)、LIMIT
语法写错; - 扩展性差:添加查询条件时,需要同时修改列表和总数的 SQL;
- 不支持复杂场景:联表查询、自定义排序的分页实现更复杂。
而 MyBatis-Plus 的分页功能,正是为解决这些痛点而生 ------ 通过插件化思想,自动拦截 SQL 并添加分页逻辑,开发者只需关注 "查询条件",无需手动处理分页细节。
二、MyBatis-Plus 分页基础:3 步实现分页查询
MyBatis-Plus 的分页功能基于PaginationInnerInterceptor
插件,核心原理是拦截 SQL 语句,自动添加分页条件(如LIMIT
)和总条数查询。使用前需完成 3 步:引入依赖、配置插件、编写代码。
2.1 第一步:引入依赖
如果项目已集成 MyBatis-Plus,无需额外引入依赖(MP 的核心包已包含分页功能)。若未集成,需在pom.xml
中添加:
xml
<!-- MyBatis-Plus核心依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version> <!-- 建议使用最新版本 -->
</dependency>
注意:MyBatis-Plus 3.4.0 + 版本的分页插件有调整,本文基于 3.5.x 版本讲解,与旧版本略有差异。
2.2 第二步:配置分页插件
MyBatis-Plus 的分页功能需要通过MybatisPlusInterceptor
注册PaginationInnerInterceptor
插件。在 Spring Boot 项目中,创建配置类:
java
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
/**
* 注册分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
// 设置数据库类型(根据实际使用的数据库调整)
paginationInterceptor.setDbType(DbType.MYSQL);
// 溢出总页数后是否进行处理(默认false,即返回最后一页)
paginationInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
关键参数说明:
setDbType(DbType.MYSQL)
:指定数据库类型,MP 会根据数据库类型生成对应的分页 SQL(如 MySQL 用LIMIT
,Oracle 用ROWNUM
);setOverflow(true)
:当pageNum
超过总页数时,返回最后一页数据(而非空),避免前端报错;- 若需要同时使用乐观锁、多租户等插件,只需在
MybatisPlusInterceptor
中继续添加即可(插件执行顺序有讲究,分页插件建议放最后)。
2.3 第三步:编写分页查询代码
配置完成后,分页查询的核心是Page
对象 ------ 通过它传递分页参数(页码、页大小),并接收分页结果(数据列表、总条数等)。
2.3.1 基础分页(无查询条件)
以查询用户列表为例,完整流程如下:
-
定义实体类:
java
@Data @TableName("user") // 对应数据库表名 public class User { private Long id; private String name; private Integer age; private String email; private LocalDateTime createTime; }
-
编写 Mapper 接口:
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper extends BaseMapper<User> { // 继承BaseMapper后,无需手动定义方法,直接使用父类的selectPage方法 }
-
Service 层实现:
java
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.stereotype.Service; @Service public class UserService { private final UserMapper userMapper; // 构造器注入(推荐) public UserService(UserMapper userMapper) { this.userMapper = userMapper; } /** * 基础分页查询 * @param pageNum 页码(从1开始) * @param pageSize 每页条数 * @return 分页结果 */ public IPage<User> getUserPage(Integer pageNum, Integer pageSize) { // 1. 创建Page对象,传入分页参数 Page<User> page = new Page<>(pageNum, pageSize); // 2. 调用BaseMapper的selectPage方法,自动分页 // 第一个参数:Page对象(用于传递参数和接收结果) // 第二个参数:查询条件(null表示无条件) return userMapper.selectPage(page, null); } }
-
Controller 层接收并返回结果:
java
import com.baomidou.mybatisplus.core.metadata.IPage; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping("/users") public IPage<User> getUsers( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize ) { return userService.getUserPage(pageNum, pageSize); } }
-
测试结果 :
访问
http://localhost:8080/users?pageNum=1&pageSize=10
,返回 JSON 格式的分页结果:json
{ "records": [ {"id": 1, "name": "张三", "age": 20, "email": "zhangsan@example.com", "createTime": "2023-01-01T00:00:00"}, // ... 更多用户数据 ], "total": 100, // 总条数 "size": 10, // 每页条数 "current": 1, // 当前页码 "pages": 10, // 总页数 "hasNext": true, // 是否有下一页 "hasPrevious": false // 是否有上一页 }
IPage
接口(Page
是其实现类)包含了分页所需的所有信息,无需手动封装,直接返回给前端即可。
2.3.2 带条件的分页查询
实际开发中,分页往往需要结合查询条件(如按年龄筛选、按创建时间排序)。此时只需在selectPage
方法中传入QueryWrapper
对象即可。
示例:查询年龄大于 18 且邮箱不为空的用户,按创建时间倒序
java
public IPage<User> getUserByCondition(
Integer pageNum,
Integer pageSize,
Integer minAge,
String email
) {
Page<User> page = new Page<>(pageNum, pageSize);
// 构建查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 年龄大于minAge(若minAge不为null)
if (minAge != null) {
queryWrapper.gt("age", minAge);
}
// 邮箱不为空
queryWrapper.isNotNull("email");
// 按创建时间倒序排序
queryWrapper.orderByDesc("create_time");
// 执行分页查询
return userMapper.selectPage(page, queryWrapper);
}
Controller 层调用:
java
@GetMapping("/users/condition")
public IPage<User> getUsersByCondition(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) String email
) {
return userService.getUserByCondition(pageNum, pageSize, minAge, email);
}
生成的 SQL 解析 :
MP 会自动拦截查询,生成如下 SQL(以 MySQL 为例):
sql
-- 查询数据列表(带条件和排序)
SELECT id,name,age,email,create_time
FROM user
WHERE age > 18 AND email IS NOT NULL
ORDER BY create_time DESC
LIMIT 0,10;
-- 自动查询总条数(条件与列表查询一致)
SELECT COUNT(*)
FROM user
WHERE age > 18 AND email IS NOT NULL;
可以看到,条件和排序会同时应用到 "数据查询" 和 "总条数查询" 中,无需手动编写两条 SQL,大幅减少冗余。
三、进阶用法:自定义 SQL 的分页查询
虽然 MP 的QueryWrapper
能满足大部分条件查询,但复杂场景(如联表查询、子查询)仍需自定义 SQL。此时只需在 Mapper 接口中传入IPage
参数,MP 会自动为自定义 SQL 添加分页逻辑。
3.1 XML 方式自定义 SQL 分页
场景 :查询用户及其关联的角色信息(联表查询user
和user_role
表),实现分页。
3.1.1 定义 VO(返回结果封装)
java
@Data
public class UserRoleVO {
private Long userId;
private String userName;
private Integer age;
private String roleName; // 角色名称
private LocalDateTime createTime;
}
3.1.2 Mapper 接口定义
java
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 自定义分页查询(联表查询用户和角色)
* @param page 分页参数
* @param roleName 角色名称(查询条件)
* @return 分页结果
*/
IPage<UserRoleVO> selectUserRolePage(
Page<UserRoleVO> page,
@Param("roleName") String roleName
);
}
关键 :方法第一个参数必须是Page
对象(用于接收分页参数和结果),后续参数为查询条件(需用@Param
指定参数名)。
3.1.3 XML 映射文件编写
xml
<mapper namespace="com.example.mapper.UserMapper">
<!-- 自定义分页查询SQL -->
<select id="selectUserRolePage" resultType="com.example.vo.UserRoleVO">
SELECT
u.id AS userId,
u.name AS userName,
u.age,
r.name AS roleName,
u.create_time AS createTime
FROM
user u
LEFT JOIN
user_role ur ON u.id = ur.user_id
LEFT JOIN
role r ON ur.role_id = r.id
<!-- 动态条件:角色名称模糊查询 -->
<where>
<if test="roleName != null and roleName != ''">
r.name LIKE CONCAT('%', #{roleName}, '%')
</if>
</where>
<!-- 排序 -->
ORDER BY u.create_time DESC
</select>
</mapper>
注意 :XML 中无需手动添加LIMIT
语句,MP 会自动拦截 SQL 并添加分页条件(根据数据库类型)。
3.1.4 Service 层调用
java
public IPage<UserRoleVO> getUserRolePage(
Integer pageNum,
Integer pageSize,
String roleName
) {
Page<UserRoleVO> page = new Page<>(pageNum, pageSize);
return userMapper.selectUserRolePage(page, roleName);
}
3.1.5 生成的 SQL 解析
当调用selectUserRolePage
时,MP 会自动生成两条 SQL:
-
数据查询 SQL (添加
LIMIT
):sql
SELECT u.id AS userId, u.name AS userName, u.age, r.name AS roleName, u.create_time AS createTime FROM user u LEFT JOIN user_role ur ON u.id = ur.user_id LEFT JOIN role r ON ur.role_id = r.id WHERE r.name LIKE CONCAT('%', '管理员', '%') ORDER BY u.create_time DESC LIMIT 0, 10;
-
总条数查询 SQL (自动生成
COUNT(*)
):sql
SELECT COUNT(*) FROM user u LEFT JOIN user_role ur ON u.id = ur.user_id LEFT JOIN role r ON ur.role_id = r.id WHERE r.name LIKE CONCAT('%', '管理员', '%');
这意味着,即使是自定义 SQL,MP 也能自动处理分页逻辑,开发者只需关注核心查询逻辑即可。
3.2 注解方式自定义 SQL 分页
除了 XML,也可以用@Select
注解编写自定义 SQL,适合简单的查询场景。
示例:用注解实现用户 - 角色联表分页查询
java
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 注解方式自定义分页查询
*/
@Select("SELECT " +
"u.id AS userId, u.name AS userName, u.age, " +
"r.name AS roleName, u.create_time AS createTime " +
"FROM user u " +
"LEFT JOIN user_role ur ON u.id = ur.user_id " +
"LEFT JOIN role r ON ur.role_id = r.id " +
"WHERE r.name LIKE CONCAT('%', #{roleName}, '%') " +
"ORDER BY u.create_time DESC")
IPage<UserRoleVO> selectUserRolePageByAnnotation(
Page<UserRoleVO> page,
@Param("roleName") String roleName
);
}
用法与 XML 方式一致,Service 层直接调用即可。对于复杂 SQL,建议优先使用 XML 方式(可读性更好)。
四、高级特性:让分页查询更灵活
MyBatis-Plus 的分页功能远不止基础查询,还支持结果自定义、分页插件细粒度配置、逻辑删除分页等高级特性,满足复杂业务场景。
4.1 分页结果自定义(封装 VO)
默认的IPage
结果包含records
、total
等字段,但若前端需要特定格式(如data
、totalCount
),可以手动封装分页结果。
示例:自定义分页响应 VO
java
@Data
public class PageResponse<T> {
private int code = 200; // 状态码
private String message = "success"; // 提示信息
private long total; // 总条数
private int pageNum; // 当前页码
private int pageSize; // 每页条数
private List<T> data; // 数据列表
// 构造方法:从IPage转换
public PageResponse(IPage<T> page) {
this.total = page.getTotal();
this.pageNum = (int) page.getCurrent();
this.pageSize = (int) page.getSize();
this.data = page.getRecords();
}
// 静态工厂方法(更简洁)
public static <T> PageResponse<T> of(IPage<T> page) {
return new PageResponse<>(page);
}
}
Service 层调用:
java
public PageResponse<UserRoleVO> getUserRolePageCustom(
Integer pageNum,
Integer pageSize,
String roleName
) {
Page<UserRoleVO> page = new Page<>(pageNum, pageSize);
IPage<UserRoleVO> resultPage = userMapper.selectUserRolePage(page, roleName);
return PageResponse.of(resultPage);
}
返回给前端的结果更符合业务规范:
json
{
"code": 200,
"message": "success",
"total": 50,
"pageNum": 1,
"pageSize": 10,
"data": [/* 用户角色数据 */]
}
4.2 分页插件的细粒度配置
全局配置分页插件后,若某些方法需要特殊配置(如不同的页大小限制、溢出处理策略),可以通过Page
对象的方法动态调整。
4.2.1 单个查询设置最大页大小
防止恶意请求(如pageSize=10000
导致性能问题),可以在Page
对象中设置最大页大小:
java
public IPage<User> getUserWithMaxSize(Integer pageNum, Integer pageSize) {
// 设置最大页大小为100,若传入的pageSize>100,则强制使用100
Page<User> page = new Page<>(pageNum, pageSize)
.setSearchCount(true) // 是否查询总条数(默认true,false则不查total)
.setMaxLimit(100L); // 最大页大小限制
return userMapper.selectPage(page, null);
}
4.2.2 关闭总条数查询(提升性能)
某些场景下(如滚动加载、只需要数据列表),可以关闭总条数查询(setSearchCount(false)
),减少一次 SQL 执行,提升性能:
java
public IPage<User> getUserWithoutTotal(Integer pageNum, Integer pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
page.setSearchCount(false); // 不查询总条数(total=0,pages=0)
return userMapper.selectPage(page, null);
}
生成的 SQL 只会有数据查询(无COUNT(*)
查询):
sql
SELECT id,name,age,email,create_time FROM user LIMIT 0,10;
4.3 结合 Lambda 表达式的类型安全查询
QueryWrapper
虽然灵活,但字符串列名(如"age"
)容易写错(编译期不报错,运行期才发现)。MP 的LambdaQueryWrapper
通过 Lambda 表达式引用实体类字段,实现类型安全的查询。
示例:用 LambdaQueryWrapper 实现条件分页
java
public IPage<User> getUserByLambda(Integer pageNum, Integer pageSize, Integer age) {
Page<User> page = new Page<>(pageNum, pageSize);
// 用LambdaQueryWrapper避免硬编码列名
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
if (age != null) {
lambdaWrapper.gt(User::getAge, age); // 引用User类的age字段(编译期检查)
}
lambdaWrapper.isNotNull(User::getEmail) // 等价于email IS NOT NULL
.orderByDesc(User::getCreateTime); // 按createTime倒序
return userMapper.selectPage(page, lambdaWrapper);
}
优势 :若实体类字段名修改(如age
改为userAge
),编译器会直接报错,避免线上问题。
4.4 逻辑删除的分页查询
MyBatis-Plus 的逻辑删除(通过@TableLogic
注解)会自动在查询中添加deleted=0
条件,分页查询也会自动适配,无需额外处理。
示例:
-
实体类添加逻辑删除字段:
java
@Data @TableName("user") public class User { private Long id; private String name; private Integer age; // 逻辑删除字段(0=未删除,1=已删除) @TableLogic private Integer deleted; }
-
分页查询:
java
public IPage<User> getDeletedUser(Integer pageNum, Integer pageSize) { Page<User> page = new Page<>(pageNum, pageSize); // 查询已删除的用户(手动添加deleted=1条件) QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("deleted", 1); return userMapper.selectPage(page, queryWrapper); }
若不添加
eq("deleted", 1)
,默认查询的是deleted=0
的未删除数据(逻辑删除的全局配置生效)。分页查询会自动包含逻辑删除条件,与普通查询一致。
4.5 多表联表分页的性能优化
联表分页查询(尤其是多表 JOIN)容易出现性能问题,可通过以下方式优化:
- 确保关联字段有索引 :如
user_role.user_id
、role.id
需建立索引; - ** 避免 SELECT ***:只查询需要的字段,减少数据传输;
- 子查询优化:将复杂 JOIN 改为子查询,减少关联表数量;
- 分页参数合理化 :限制最大
pageSize
(如不超过 1000),避免一次查询过多数据。
示例:优化后的联表分页查询
xml
<select id="selectUserRolePageOptimized" resultType="com.example.vo.UserRoleVO">
SELECT
u.id AS userId,
u.name AS userName,
u.age,
(SELECT r.name FROM role r WHERE r.id = ur.role_id) AS roleName,
u.create_time AS createTime
FROM
user u
LEFT JOIN
user_role ur ON u.id = ur.user_id
<where>
<if test="roleName != null">
EXISTS (
SELECT 1 FROM role r WHERE r.id = ur.role_id AND r.name LIKE CONCAT('%', #{roleName}, '%')
)
</if>
</where>
ORDER BY u.create_time DESC
</select>
通过子查询和EXISTS
替代多表 JOIN,减少关联次数,提升分页查询效率。
五、避坑指南:分页查询常见问题及解决方案
在使用 MyBatis-Plus 分页功能时,可能会遇到一些 "坑",这里总结了最常见的问题及解决方案。
5.1 分页插件不生效(查询所有数据,无分页)
现象 :调用selectPage
后,返回所有数据(total
等于总条数,但records
包含全部数据,未分页)。
可能原因及解决:
- 未配置分页插件 :检查是否在
MybatisPlusInterceptor
中添加了PaginationInnerInterceptor
; - 插件顺序错误 :若同时配置了其他插件(如
IllegalSQLInnerInterceptor
),分页插件可能被覆盖,建议分页插件放最后; - Mapper 接口未继承 BaseMapper :自定义 Mapper 接口需继承
BaseMapper
,或手动定义selectPage
方法; - Page 对象未作为第一个参数 :自定义方法中,
Page
对象必须是第一个参数,否则 MP 无法识别。
5.2 总条数查询错误(total 与实际不符)
现象 :分页结果的total
值与实际总条数不符(如实际 100 条,返回 50 条)。
可能原因及解决:
- 查询条件不一致 :自定义 SQL 中,数据查询和总条数查询的条件不一致(MP 会自动复用 WHERE 条件,若手动写 COUNT 则可能出错);
- 解决方案:避免手动编写 COUNT 语句,让 MP 自动生成;
- 逻辑删除配置错误 :若实体类有逻辑删除字段但未配置
@TableLogic
,总条数会包含已删除数据;- 解决方案:添加
@TableLogic
注解,或在条件中手动排除已删除数据。
- 解决方案:添加
5.3 分页查询性能差(耗时过长)
现象:分页查询耗时超过 1 秒,甚至超时。
优化方案:
- 添加索引 :确保 WHERE 条件、ORDER BY 的字段有索引(如
age
、create_time
); - 避免全表扫描 :检查 SQL 是否走索引(用
EXPLAIN
分析),避免SELECT *
; - 限制最大页大小 :通过
setMaxLimit(1000)
防止pageSize
过大; - 关闭总条数查询 :非必要时用
setSearchCount(false)
减少一次 SQL; - 分库分表场景:若数据量超千万级,建议结合分库分表中间件(如 ShardingSphere),MP 分页只作用于单表。
5.4 多数据源场景下分页插件不生效
现象:项目使用多数据源(如 dynamic-datasource-spring-boot-starter),部分数据源分页不生效。
解决方案 :
分页插件需在每个数据源的 MyBatis 配置中单独注册,或使用全局配置(确保插件被所有数据源共享)。
示例(多数据源配置分页插件):
java
@Configuration
public class MultiDataSourceConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
// 数据源1配置
@Bean
@ConfigurationProperties("spring.datasource.dynamic.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
// 数据源2配置
@Bean
@ConfigurationProperties("spring.datasource.dynamic.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
// 确保分页插件被所有数据源使用
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactory(DataSource dataSource, MybatisPlusInterceptor interceptor) throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setPlugins(interceptor); // 注入分页插件
return sessionFactory;
}
}
六、最佳实践:分页查询的 "黄金法则"
结合实际项目经验,总结以下最佳实践,让分页查询更高效、更可靠。
6.1 分页参数校验不可少
前端传入的pageNum
和pageSize
可能为负数或过大,需在 Controller 层进行校验:
java
@GetMapping("/users")
public IPage<User> getUsers(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize
) {
// 校验页码(至少为1)
pageNum = Math.max(pageNum, 1);
// 校验页大小(1-1000之间)
pageSize = Math.min(Math.max(pageSize, 1), 1000);
return userService.getUserPage(pageNum, pageSize);
}
避免因参数错误导致的异常(如pageNum=0
、pageSize=10000
)。
6.2 优先使用 BaseMapper 的分页方法
BaseMapper 提供的selectPage
、selectMapsPage
(返回 Map 结果)等方法已足够满足大部分场景,尽量避免重复造轮子。
java
// 查询Map结果(无需定义VO)
public IPage<Map<String, Object>> getUserMapPage(Integer pageNum, Integer pageSize) {
Page<Map<String, Object>> page = new Page<>(pageNum, pageSize);
return userMapper.selectMapsPage(page, new QueryWrapper<User>().select("id", "name", "age"));
}
6.3 复杂场景考虑 "游标分页"
传统分页(基于pageNum
和pageSize
)在数据量超大(如 1000 万 +)且频繁翻页时,性能会下降(因为LIMIT 1000000, 10
需要扫描前 100 万行)。此时可采用 "游标分页"(基于上次查询的最后一条记录的 ID)。
示例:游标分页实现
java
public IPage<User> getUserByCursor(
Integer pageSize,
@RequestParam(required = false) Long lastId
) {
Page<User> page = new Page<>(1, pageSize); // 页码固定为1,用lastId作为游标
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 若有lastId,查询ID大于lastId的数据(假设ID自增)
if (lastId != null) {
queryWrapper.gt("id", lastId);
}
queryWrapper.orderByAsc("id"); // 按ID排序,确保游标有效
return userMapper.selectPage(page, queryWrapper);
}
前端通过每次返回的最后一条记录的id
作为下一次查询的lastId
,实现高效的滚动加载。这种方式适合移动端列表、日志查询等场景,但不支持 "跳页"(如直接到第 10 页)。
6.4 分页查询与缓存结合
对于高频且变化不频繁的分页查询(如商品列表),可以结合缓存(如 Redis)提升性能:
java
public IPage<Product> getProductPage(Integer pageNum, Integer pageSize) {
// 缓存key(包含页码和页大小)
String cacheKey = "product:page:" + pageNum + ":" + pageSize;
// 从Redis获取缓存
IPage<Product> cachedPage = redisTemplate.opsForValue().get(cacheKey);
if (cachedPage != null) {
return cachedPage;
}
// 缓存未命中,查询数据库
Page<Product> page = new Page<>(pageNum, pageSize);
IPage<Product> resultPage = productMapper.selectPage(page, null);
// 存入Redis(设置10分钟过期)
redisTemplate.opsForValue().set(cacheKey, resultPage, 10, TimeUnit.MINUTES);
return resultPage;
}
注意:缓存需根据数据更新策略(如商品修改后清除对应缓存)及时失效,避免返回脏数据。
六、总结:MyBatis-Plus 分页 ------ 简单与强大的完美结合
MyBatis-Plus 的分页功能彻底改变了传统分页查询的开发模式:从 "编写两条 SQL + 手动计算分页参数" 到 "一行代码完成分页",大幅减少了冗余代码,降低了出错概率。无论是基础的条件分页、复杂的联表查询,还是高性能的游标分页,MP 都能提供简洁高效的解决方案。