MyBatis-Plus分页查询案例

wen java案例 3

MyBatis-Plus分页查询实战:从配置到性能优化的完整指南

目录导读

  1. 为什么分页查询是Web开发的“刚需”
  2. MyBatis-Plus分页插件核心配置(附代码)
  3. 三种分页写法对比:传统 vs 内置 vs 自定义
  4. 高频面试题:分页SQL的常见坑(问与答)
  5. 性能调优:百万级数据分页不卡顿的5个技巧
  6. 什么时候该用物理分页,什么时候该用逻辑分页

为什么分页查询是Web开发的“刚需”

想象一下,你的用户表有500万条记录,如果一次性全查出来,浏览器直接崩溃——这不是段子,是线上事故,分页查询的核心价值在于减少数据传输量降低数据库负荷

MyBatis-Plus分页查询案例

MyBatis-Plus作为国内最流行的MyBatis增强框架,把分页做成了“开箱即用”的功能,但很多新手只知其然不知其所以然,今天我们就通过一个完整的电商订单分页案例,把分页的底层逻辑、性能调优、常见坑一次性讲透。


MyBatis-Plus分页插件核心配置(附代码)

1 Maven依赖(版本号以最新稳定版为准)

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

2 配置分页插件(重要!不配报错)

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 指定数据库类型为MySQL
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

关键点:如果不配置这个拦截器,Page对象只会查询全部数据然后内存截取,等于没分页。

3 实体类与Mapper

// 实体类省略getter/setter
@Data
public class Order {
    private Long id;
    private String orderNo;
    private BigDecimal amount;
    private Integer status;
    private Date createTime;
}
public interface OrderMapper extends BaseMapper<Order> {
    // 自定义分页方法(后面会讲)
    IPage<Order> selectOrderPage(Page<?> page, @Param("status") Integer status);
}

三种分页写法对比:传统 vs 内置 vs 自定义

1 传统方式(自己写Limit)——不推荐

List<Order> list = orderMapper.selectList(
    new QueryWrapper<Order>().last("LIMIT " + offset + ", " + size));

问题:SQL注入风险、数据库兼容性差、需要手工计算offset。

2 MyBatis-Plus内置Page(最常用)

// 查询第2页,每页10条
Page<Order> page = new Page<>(2, 10);
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getStatus, 1)
       .orderByDesc(Order::getCreateTime);
IPage<Order> result = orderMapper.selectPage(page, wrapper);
System.out.println("总记录数:" + result.getTotal());
System.out.println("当前页数据:" + result.getRecords());

3 自定义SQL分页(适合多表联查)

<!-- OrderMapper.xml -->
<select id="selectOrderPage" resultType="com.example.entity.Order">
    SELECT o.*, u.name AS userName
    FROM orders o
    LEFT JOIN user u ON o.user_id = u.id
    WHERE o.status = #{status}
</select>
// Mapper接口方法
IPage<Order> selectOrderPage(Page<?> page, @Param("status") Integer status);
// 调用(注意:Page参数必须放在第一个位置)
Page<Order> page = new Page<>(1, 10);
IPage<Order> result = orderMapper.selectOrderPage(page, 1);

底层原理:MyBatis-Plus会自动改写SQL,在原SQL尾部追加LIMIT,并执行一次COUNT查询。


高频面试题:分页SQL的常见坑(问与答)

Q1:为什么我的分页查询很慢,明明加了索引?

:检查你用的LIMIT offset, size,当offset很大时(比如第100万条),MySQL依然要扫描前100万行,解决方案是延迟关联子查询

-- 传统写法
SELECT * FROM orders WHERE status=1 LIMIT 100000, 20;
-- 优化写法(先走覆盖索引获取ID,再回表)
SELECT * FROM orders 
WHERE status=1 AND id > (
    SELECT id FROM orders WHERE status=1 ORDER BY id LIMIT 100000, 1
) LIMIT 20;

Q2:MyBatis-Plus分页和手写Limit有什么区别?

:MyBatis-Plus通过PaginationInnerInterceptor拦截器,自动生成COUNT和LIMIT语句。核心优势

  • 自动处理不同数据库方言(MySQL用LIMIT,Oracle用ROWNUM)
  • 返回的IPage对象自带totalpages等分页元数据
  • 不用手动拼接COUNT查询

Q3:分页时COUNT查询性能差怎么办?

:如果表数据量极大,COUNT全表扫描很耗时,可考虑:

// 配置不走COUNT,返回的total为0(需自行判断是否需要总页数)
page.setSearchCount(false);

或者用SELECT COUNT(*)改为SELECT COUNT(id)(走索引更快)。


性能调优:百万级数据分页不卡顿的5个技巧

1 强制走覆盖索引

WHEREORDER BY字段上建立联合索引,避免filesort

2 避免“深分页”

产品上限制最大页码(例如只能翻到第1000页),超过则提示用“加载更多”或“日期区间查询”。

3 使用游标分页(Keyset Pagination)

适用于实时性高的场景(如用户动态流),不依赖页码,只记录上次最后一条的ID:

WHERE id < 1000 ORDER BY id DESC LIMIT 20

4 开启MyBatis-Plus的分页插件缓存

在插件中设置optimizeJoin为true,可优化COUNT语句的JOIN。

5 必要时不用COUNT

业务上如果展示“加载更多”,无需返回总条数,用page.setSearchCount(false)减少一次查询。


什么时候该用物理分页,什么时候该用逻辑分页

场景 推荐方式 原因
后台管理系统表格 物理分页(MyBatis-Plus) 数据量大,需精确页码
移动端“上拉加载” 游标分页 避免深分页,体验好
数据量<1000条 逻辑分页(一次查全部分页) 减少连接交互,简单快捷

最后提醒:分页不是简单的“查一页数据”,而是数据库IO、网络传输、业务交互的综合权衡,用MyBatis-Plus,一定要配好拦截器,理解COUNT的代价,并在SQL层面做索引优化。


(文中所有代码基于MyBatis-Plus 3.5.x版本,不同版本API略有差异,请以官方文档为准。)

抱歉,评论功能暂时关闭!