从基础到进阶:MyBatis-Plus 分页查询封神指南

发布于:2025-07-17 ⋅ 阅读:(19) ⋅ 点赞:(0)

在 Java 后端开发中,分页查询是高频需求 —— 无论是用户列表、订单记录还是数据分析,都需要通过分页来限制数据量,提升接口响应速度和用户体验。传统的 MyBatis 实现分页需要手动编写LIMIT语句、计算总条数,代码冗余且容易出错。而 MyBatis-Plus(简称 MP)的分页功能,能让这一切变得简单:一行配置、一行代码,就能完成分页查询,甚至支持复杂条件、自定义 SQL、联表查询等场景。

本文将从基础用法到高级技巧,全方位解析 MyBatis-Plus 的分页功能,结合 20 + 实战案例,让你彻底掌握如何用 MP 写出简洁高效的分页代码,从此告别分页查询的繁琐操作。

一、为什么说传统分页是 “体力活”?

在介绍 MyBatis-Plus 的分页之前,我们先回顾一下传统 MyBatis 实现分页的流程。假设要查询 “第 2 页的用户列表,每页 10 条,按创建时间倒序”,你需要做这些事:

  1. 编写 Mapper 接口:定义两个方法,一个查列表,一个查总条数;
  2. 编写 XML 映射:手动拼接LIMIT语句(LIMIT 10 OFFSET 10),总条数查询用COUNT(*)
  3. 处理分页参数:在 Service 层计算offset = (pageNum - 1) * pageSize
  4. 封装分页结果:将 “列表数据 + 总条数 + 页码 + 页大小” 封装到分页对象中。

传统 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 基础分页(无查询条件)

以查询用户列表为例,完整流程如下:

  1. 定义实体类

    java

    @Data
    @TableName("user") // 对应数据库表名
    public class User {
        private Long id;
        private String name;
        private Integer age;
        private String email;
        private LocalDateTime createTime;
    }
    
  2. 编写 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方法
    }
    
  3. 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);
        }
    }
    
  4. 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);
        }
    }
    
  5. 测试结果
    访问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 分页

场景:查询用户及其关联的角色信息(联表查询useruser_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:

  1. 数据查询 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;
    
  2. 总条数查询 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结果包含recordstotal等字段,但若前端需要特定格式(如datatotalCount),可以手动封装分页结果。

示例:自定义分页响应 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条件,分页查询也会自动适配,无需额外处理。

示例

  1. 实体类添加逻辑删除字段

    java

    @Data
    @TableName("user")
    public class User {
        private Long id;
        private String name;
        private Integer age;
        // 逻辑删除字段(0=未删除,1=已删除)
        @TableLogic
        private Integer deleted;
    }
    
  2. 分页查询

    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)容易出现性能问题,可通过以下方式优化:

  1. 确保关联字段有索引:如user_role.user_idrole.id需建立索引;
  2. ** 避免 SELECT ***:只查询需要的字段,减少数据传输;
  3. 子查询优化:将复杂 JOIN 改为子查询,减少关联表数量;
  4. 分页参数合理化:限制最大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包含全部数据,未分页)。

可能原因及解决

  1. 未配置分页插件:检查是否在MybatisPlusInterceptor中添加了PaginationInnerInterceptor
  2. 插件顺序错误:若同时配置了其他插件(如IllegalSQLInnerInterceptor),分页插件可能被覆盖,建议分页插件放最后;
  3. Mapper 接口未继承 BaseMapper:自定义 Mapper 接口需继承BaseMapper,或手动定义selectPage方法;
  4. Page 对象未作为第一个参数:自定义方法中,Page对象必须是第一个参数,否则 MP 无法识别。

5.2 总条数查询错误(total 与实际不符)

现象:分页结果的total值与实际总条数不符(如实际 100 条,返回 50 条)。

可能原因及解决

  1. 查询条件不一致:自定义 SQL 中,数据查询和总条数查询的条件不一致(MP 会自动复用 WHERE 条件,若手动写 COUNT 则可能出错);
    • 解决方案:避免手动编写 COUNT 语句,让 MP 自动生成;
  2. 逻辑删除配置错误:若实体类有逻辑删除字段但未配置@TableLogic,总条数会包含已删除数据;
    • 解决方案:添加@TableLogic注解,或在条件中手动排除已删除数据。

5.3 分页查询性能差(耗时过长)

现象:分页查询耗时超过 1 秒,甚至超时。

优化方案

  1. 添加索引:确保 WHERE 条件、ORDER BY 的字段有索引(如agecreate_time);
  2. 避免全表扫描:检查 SQL 是否走索引(用EXPLAIN分析),避免SELECT *
  3. 限制最大页大小:通过setMaxLimit(1000)防止pageSize过大;
  4. 关闭总条数查询:非必要时用setSearchCount(false)减少一次 SQL;
  5. 分库分表场景:若数据量超千万级,建议结合分库分表中间件(如 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 分页参数校验不可少

前端传入的pageNumpageSize可能为负数或过大,需在 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=0pageSize=10000)。

6.2 优先使用 BaseMapper 的分页方法

BaseMapper 提供的selectPageselectMapsPage(返回 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 复杂场景考虑 “游标分页”

传统分页(基于pageNumpageSize)在数据量超大(如 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 都能提供简洁高效的解决方案。


网站公告

今日签到

点亮在社区的每一天
去签到