一、核心概念
IPage<T>
接口: MyBatis-Plus 分页的核心接口,代表一个分页结果。它包含了当前页码、每页大小、总记录数、总页数以及当前页的数据列表等信息。Page<T>
类:IPage<T>
的常用实现类。在查询前,我们创建一个Page
对象,设置current
(当前页) 和size
(每页大小)。查询后,MyBatis-Plus 会自动填充total
,pages
,records
等字段。PaginationInnerInterceptor
: MyBatis-Plus 的分页插件(在新版本中取代了旧的PaginationInterceptor
)。它是一个 MyBatis 的Interceptor
,负责拦截 SQL 执行,在 SQL 语句前后自动添加COUNT
查询和LIMIT/OFFSET
(或数据库特定的分页语法,如ROW_NUMBER()
)。- 自定义分页方法: 指在 Mapper 接口中定义一个返回类型为
IPage<T>
或Page<T>
的方法,并在对应的 XML 文件或使用@Select
注解中编写自定义的复杂 SQL。当调用此方法时,PaginationInnerInterceptor
会自动生效,实现分页。 @Select
,@Results
,@ResultMap
: 用于在 Mapper 接口上直接编写 SQL 注解。@Results
或@ResultMap
用于处理复杂的结果映射(如多表关联字段映射到 DTO)。- XML 映射文件: 更常用和灵活的方式,将复杂的 SQL 写在 XML 文件中,便于维护和调试。
二、详细操作步骤
以下是创建和使用自定义分页查询方法的完整、详细步骤:
步骤 1: 引入依赖并配置分页插件
确保 mybatis-plus-boot-starter
依赖已引入。最关键的是配置 PaginationInnerInterceptor
。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
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 {
/**
* 配置 MyBatis-Plus 插件
* @return MybatisPlusInterceptor
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); // 指定数据库类型
// paginationInnerInterceptor.setOverflow(false); // 溢出处理,默认false,即查询页数超过总页数时,查询最后一页。true则进行处理(如返回空或第一页)
// paginationInnerInterceptor.setMaxLimit(100L); // 单页分页条数限制,-1 不受限制
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
注意: 旧版本使用 PaginationInterceptor
,新版本(3.4.0+)推荐使用 MybatisPlusInterceptor
+ PaginationInnerInterceptor
。
步骤 2: 创建实体类 (Entity) 和数据传输对象 (DTO)
假设我们有 User
和 Order
表。
User 实体类:
import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
@TableName("user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("name")
private String name;
@TableField("email")
private String email;
// 省略 Getter、Setter
public User() {}
// ... (Getter and Setter)
}
Order 实体类:
import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@TableName("`order`")
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("user_id")
private Long userId;
@TableField("order_no")
private String orderNo;
@TableField("amount")
private BigDecimal amount;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("status")
private Integer status;
// 省略 Getter、Setter
public Order() {}
// ... (Getter and Setter)
}
自定义分页查询结果 DTO (UserOrderDTO):
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 用于接收用户及其订单信息的分页查询结果
*/
public class UserOrderDTO implements Serializable {
private static final long serialVersionUID = 1L;
private Long userId;
private String userName;
private String userEmail;
private Long orderId;
private String orderNo;
private BigDecimal orderAmount;
private LocalDateTime orderCreateTime;
private Integer orderStatus;
// 省略 Getter、Setter、构造函数
public UserOrderDTO() {}
// ... (Getter and Setter)
}
步骤 3: 创建 Mapper 接口
创建一个 Mapper 接口,定义自定义分页查询方法。该方法的返回类型必须是 IPage<T>
或 Page<T>
,并且第一个参数必须是 IPage<T>
类型的分页对象。
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserOrderMapper extends BaseMapper<User> {
/**
* 自定义分页查询:获取用户及其最新订单信息
* @param page 分页对象 (必须是第一个参数)
* @param status 订单状态 (可选筛选条件)
* @return 分页结果 IPage<UserOrderDTO>
*/
IPage<UserOrderDTO> selectUserOrderPage(IPage<UserOrderDTO> page, @Param("status") Integer status);
/**
* 另一种写法:返回 Page<UserOrderDTO>
* @param page 分页对象
* @param minAmount 订单金额下限
* @return 分页结果 Page<UserOrderDTO>
*/
// Page<UserOrderDTO> selectUserOrderPageByAmount(Page<UserOrderDTO> page, @Param("minAmount") BigDecimal minAmount);
}
关键点:
- 返回类型:
IPage<UserOrderDTO>
或Page<UserOrderDTO>
。 - 第一个参数: 必须是
IPage<UserOrderDTO>
或Page<UserOrderDTO>
类型的分页对象。这是 MyBatis-Plus 分页插件能够识别并拦截此方法的关键。 - 其他参数: 使用
@Param("paramName")
注解,以便在 SQL 中引用。@Param
注解对于非IPage
参数是必须的,否则 MyBatis 无法正确绑定参数。
步骤 4: 编写自定义 SQL
方式一:使用 XML 映射文件 (推荐)
创建 UserOrderMapper.xml
文件,通常放在 resources/mapper/
目录下。
<?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.example.mapper.UserOrderMapper">
<!-- 定义结果映射 ResultMap -->
<resultMap id="UserOrderResultMap" type="com.example.dto.UserOrderDTO">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="userEmail" column="user_email"/>
<result property="orderId" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="orderAmount" column="order_amount"/>
<result property="orderCreateTime" column="order_create_time"/>
<result property="orderStatus" column="order_status"/>
</resultMap>
<!-- 自定义分页查询 SQL -->
<select id="selectUserOrderPage" resultMap="UserOrderResultMap">
SELECT
u.id AS user_id,
u.name AS user_name,
u.email AS user_email,
o.id AS order_id,
o.order_no,
o.amount AS order_amount,
o.create_time AS order_create_time,
o.status AS order_status
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
<where>
<if test="status != null">
AND o.status = #{status}
</if>
<!-- 可以添加更多动态条件 -->
</where>
ORDER BY o.create_time DESC, u.id ASC
<!-- 注意:这里不需要写 LIMIT/OFFSET,分页插件会自动添加 -->
</select>
</mapper>
关键点:
namespace
: 必须与 Mapper 接口的全限定名一致。id
: 必须与 Mapper 接口中方法名一致。resultMap
: 强烈推荐使用resultMap
来处理复杂的字段映射,避免SELECT *
和列名冲突。ORDER BY
: 建议指定排序规则,确保分页结果的稳定性。LIMIT/OFFSET
: 绝对不要手动写!PaginationInnerInterceptor
会根据IPage
对象的current
和size
自动计算OFFSET
和LIMIT
并添加到 SQL 末尾。- 动态 SQL: 可以使用
<where>
,<if>
,<choose>
,<foreach>
等 MyBatis 动态标签。
方式二:使用注解 @Select
(适用于较简单 SQL)
如果 SQL 相对简单,可以直接在 Mapper 接口上使用 @Select
注解。
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.*;
@Mapper
public interface UserOrderMapper extends BaseMapper<User> {
@Select("SELECT u.id AS user_id, u.name AS user_name, u.email AS user_email, " +
"o.id AS order_id, o.order_no, o.amount AS order_amount, " +
"o.create_time AS order_create_time, o.status AS order_status " +
"FROM user u " +
"LEFT JOIN `order` o ON u.id = o.user_id " +
"WHERE (#{status} IS NULL OR o.status = #{status}) " +
"ORDER BY o.create_time DESC, u.id ASC")
@Results(id = "userOrderMap", value = {
@Result(property = "userId", column = "user_id", id = true),
@Result(property = "userName", column = "user_name"),
@Result(property = "userEmail", column = "user_email"),
@Result(property = "orderId", column = "order_id"),
@Result(property = "orderNo", column = "order_no"),
@Result(property = "orderAmount", column = "order_amount"),
@Result(property = "orderCreateTime", column = "order_create_time"),
@Result(property = "orderStatus", column = "order_status")
})
IPage<UserOrderDTO> selectUserOrderPage(IPage<UserOrderDTO> page, @Param("status") Integer status);
}
注意: 注解方式处理复杂动态 SQL 不如 XML 方便,且 SQL 可读性较差。
步骤 5: 在 Service 中调用自定义分页方法
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserOrderService {
@Autowired
private UserOrderMapper userOrderMapper;
/**
* 获取用户订单分页列表
* @param current 当前页码 (从1开始)
* @param size 每页大小
* @param status 订单状态筛选条件
* @return 分页结果 IPage<UserOrderDTO>
*/
public IPage<UserOrderDTO> getUserOrderPage(int current, int size, Integer status) {
// 1. 创建 Page 对象
Page<UserOrderDTO> page = new Page<>(current, size); // current: 页码, size: 每页数量
// 2. 调用自定义 Mapper 方法
// 注意:传入的是 page 对象,方法返回的也是 IPage<UserOrderDTO>
IPage<UserOrderDTO> resultPage = userOrderMapper.selectUserOrderPage(page, status);
// 3. 返回结果
// resultPage 已经包含了 total, pages, current, size, records 等所有分页信息
return resultPage;
}
// 调用示例:
// IPage<UserOrderDTO> pageResult = userOrderService.getUserOrderPage(1, 10, 1);
// System.out.println("总记录数: " + pageResult.getTotal());
// System.out.println("总页数: " + pageResult.getPages());
// System.out.println("当前页数据: " + pageResult.getRecords());
}
关键点:
- 创建
Page
对象时,传入current
(页码) 和size
(每页大小)。 - 将
Page
对象作为第一个参数传递给 Mapper 方法。 - 方法返回的
IPage
对象包含了完整的分页信息,可以直接使用。
三、常见错误
分页插件未配置:
- 错误: 忘记在配置类中注册
MybatisPlusInterceptor
和PaginationInnerInterceptor
。 - 后果: 自定义分页方法不会分页,会返回所有数据(或只返回
LIMIT
但没有COUNT
,导致total
为 0)。 - 解决: 确保正确配置了分页插件。
- 错误: 忘记在配置类中注册
Mapper 方法参数顺序错误:
- 错误:
IPage
参数不在第一个位置,例如IPage<UserOrderDTO> selectUserOrderPage(@Param("status") Integer status, IPage<UserOrderDTO> page)
。 - 后果: 分页插件无法识别此方法需要分页,导致不分页。
- 解决: 必须将
IPage
参数作为方法的第一个参数。
- 错误:
缺少
@Param
注解:- 错误: 在 Mapper 方法中,非
IPage
参数缺少@Param
注解,如IPage<UserOrderDTO> selectUserOrderPage(IPage<UserOrderDTO> page, Integer status)
。 - 后果: MyBatis 无法将参数值绑定到 SQL 中的
#{status}
,导致 SQL 执行错误(参数未找到)。 - 解决: 为所有非
IPage
参数添加@Param("paramName")
注解。
- 错误: 在 Mapper 方法中,非
SQL 中手动写了
LIMIT
:- 错误: 在 XML 或注解的 SQL 中写了
LIMIT #{page.offset}, #{page.size}
或类似语句。 - 后果: 会导致分页混乱,可能执行两次
LIMIT
,或者分页插件无法正确工作。 - 解决: 绝对不要在自定义 SQL 中写
LIMIT
或OFFSET
,让分页插件自动处理。
- 错误: 在 XML 或注解的 SQL 中写了
XML 文件未被扫描:
- 错误:
UserOrderMapper.xml
文件路径错误或未在application.yml
中配置mybatis-plus.mapper-locations
。 - 后果: 报错
Invalid bound statement (not found)
。 - 解决: 确保 XML 文件在正确的目录下(如
resources/mapper/
),并在application.yml
中配置:mybatis-plus: mapper-locations: classpath:mapper/*.xml
- 错误:
resultMap
定义错误或未引用:- 错误:
resultMap
的property
和column
映射错误,或在<select>
标签中写了resultType
而不是resultMap
。 - 后果: DTO 字段无法正确赋值,为
null
。 - 解决: 仔细检查
resultMap
定义,并确保<select>
标签使用resultMap="UserOrderResultMap"
。
- 错误:
四、注意事项
IPage
参数位置: 这是硬性要求,必须是第一个参数。@Param
注解: 对于非IPage
参数,@Param
是必需的。- 不要手动分页: SQL 中禁止出现
LIMIT
,OFFSET
,ROWNUM
等分页关键字。 - 返回类型: 方法返回类型必须是
IPage<T>
或Page<T>
。 - 分页插件: 确保
PaginationInnerInterceptor
已正确配置并生效。 - 性能: 自定义 SQL 的性能取决于 SQL 本身。确保关联字段有索引,避免全表扫描。复杂的
JOIN
和ORDER BY
可能影响性能。 COUNT
查询: 分页插件会自动执行一个COUNT
查询来获取总记录数。这个COUNT
查询是基于你的主查询 SQL 生成的(通常是SELECT COUNT(*) FROM (...)
的形式)。确保这个COUNT
查询也能高效执行。ORDER BY
稳定性: 为了保证分页结果的稳定(即同一页的数据不会因为排序微小变化而跳动),ORDER BY
子句应包含一个或多个能唯一确定行顺序的字段(如主键)。
五、使用技巧
- 使用
resultMap
: 对于复杂映射,resultMap
比resultType
更强大、更清晰、更少出错。 - 动态 SQL: 充分利用 MyBatis 的动态 SQL 标签(
<if>
,<choose>
,<where>
,<set>
,<foreach>
)构建灵活的查询条件。 @Results
注解: 如果使用注解方式,可以用@Results
定义结果映射,并用@ResultMap
引用。- 复用
resultMap
: 可以在多个<select>
语句中复用同一个resultMap
。 <sql>
片段: 将常用的 SQL 片段(如公共的SELECT
列表)提取到<sql>
标签中复用。<sql id="userOrderColumns"> u.id AS user_id, u.name AS user_name, u.email AS user_email, o.id AS order_id, o.order_no, o.amount AS order_amount, o.create_time AS order_create_time, o.status AS order_status </sql> <select id="selectUserOrderPage" resultMap="UserOrderResultMap"> SELECT <include refid="userOrderColumns"/> FROM user u LEFT JOIN `order` o ON u.id = o.user_id ... </select>
@Param
命名: 使用有意义的参数名,便于 SQL 阅读。- 调试: 开启 MyBatis SQL 日志,观察生成的 SQL(包括自动添加的
LIMIT
和执行的COUNT
查询),验证分页逻辑。
六、最佳实践与性能优化
- 优先使用 XML: 对于复杂的自定义分页查询,优先使用 XML 映射文件,代码更清晰,易于维护和调试。
- 索引优化:
- 确保
JOIN
条件(如u.id = o.user_id
)中的字段有索引。 - 确保
WHERE
子句中的过滤字段(如o.status
)有索引。 - 确保
ORDER BY
子句中的字段有索引,特别是当排序字段选择性不高时。 - 考虑创建复合索引以覆盖查询(Covering Index)。
- 确保
- 优化
COUNT
查询: 如果主查询非常复杂,其衍生的COUNT
查询也可能很慢。可以考虑:- 优化主 SQL: 从根本上优化主查询。
- 使用近似值: 在某些场景下,可以接受不精确的总数,使用
SQL_CALC_FOUND_ROWS
(MySQL 旧版) 或其他方式,但这在 MyBatis-Plus 分页插件中不易直接实现。 - 业务妥协: 如果总数计算成本过高,考虑只提供“下一页”功能,而不显示总页数(但这会影响用户体验)。
- **避免 `SELECT ***: 明确指定需要的列,减少网络传输和内存消耗。
- 合理设置分页大小: 避免设置过大的
size
,防止一次性加载过多数据导致内存溢出或响应缓慢。设置合理的上限(可通过PaginationInnerInterceptor.setMaxLimit()
)。 - 深分页优化: 对于
current
非常大的深分页(如第 10000 页),OFFSET
会很大,性能很差。考虑使用游标分页 (Cursor-based Pagination) 或键集分页 (Keyset Pagination),例如WHERE id > last_seen_id ORDER BY id LIMIT size
。这需要在业务逻辑中维护上一次查询的最后一个 ID。 - 缓存: 对于不经常变化的分页数据,考虑使用 Redis 等缓存
IPage
结果,但要注意缓存失效策略。 - 异步查询: 对于耗时较长的分页查询,考虑在 Service 层使用
@Async
注解进行异步处理,避免阻塞主线程。 - 监控: 监控包含自定义分页查询的 SQL 执行时间,及时发现慢查询并优化。
通过本教程,您应该已经掌握了 MyBatis-Plus 中自定义分页查询方法的完整流程。核心在于:配置分页插件 -> Mapper 方法第一个参数为 IPage
-> 使用 @Param
-> 编写自定义 SQL (不写 LIMIT
) -> Service 中创建 Page
对象并调用。遵循最佳实践,特别是索引优化和避免深分页,可以确保您的分页查询高效稳定。