SpringBoot这个分页坑,我踩了三天才爬出来

  • SpringBoot这个分页坑,我踩了三天才爬出来*

引言

在SpringBoot开发中,分页功能几乎是每个涉及数据列表展示的项目都会用到的功能。Spring Data JPA和MyBatis等ORM框架提供了便捷的分页支持,尤其是Spring Data JPA的Pageable接口和MyBatis-Plus的IPage接口,让开发者能够快速实现分页查询。然而,在实际开发中,分页功能背后隐藏的坑却不少。最近,我在一个项目中因为分页问题整整调试了三天,最终才找到问题的根源。本文将分享这个踩坑经历,并深入分析SpringBoot分页的常见问题及其解决方案。

主体

1. 分页的基本实现

在SpringBoot中,分页的实现通常依赖于以下两种方式:

1.1 Spring Data JPA的分页

Spring Data JPA提供了Pageable接口和Page类,开发者可以通过以下方式实现分页:

java 复制代码
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findAll(Pageable pageable);
}

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public Page<User> getUsers(int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        return userRepository.findAll(pageable);
    }
}

1.2 MyBatis-Plus的分页

MyBatis-Plus提供了IPage接口和Page类,分页实现如下:

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public IPage<User> getUsers(int page, int size) {
        Page<User> pageParam = new Page<>(page, size);
        return userMapper.selectPage(pageParam, null);
    }
}

这两种方式看似简单,但在实际使用中却容易遇到各种问题。


2. 分页的常见坑点

2.1 分页参数传递问题

  • 问题描述 *: 在前端传递分页参数时,通常是通过page和size两个参数。然而,如果前端传递的参数值为0或负数,分页逻辑可能会出现问题。

  • 坑点分析*:

  • Spring Data JPA的PageRequest.of(page, size)方法中,page参数是从0开始的,但如果传递的page为负数,会抛出IllegalArgumentException。
  • MyBatis-Plus的Page类虽然对负数有默认处理(page < 1时会自动设置为1),但如果size为负数或0,可能会导致查询结果异常。
  • 解决方案*: 在Service层对分页参数进行校验:
java 复制代码
public Page<User> getUsers(int page, int size) {
    if (page < 0) page = 0;
    if (size <= 0) size = 10; // 默认每页10条
    Pageable pageable = PageRequest.of(page, size);
    return userRepository.findAll(pageable);
}

2.2 分页查询的性能问题

  • 问题描述*: 在数据量较大的表中,分页查询(尤其是深分页)可能会导致性能问题。例如,查询第1000页的数据时,数据库需要扫描并跳过前999页的数据,效率极低。

  • 坑点分析*:

  • MySQL的LIMIT offset, size在offset较大时性能极差。
  • Spring Data JPA和MyBatis-Plus的默认分页实现都是基于LIMIT,因此同样会面临这个问题。
  • 解决方案*:
  • 使用"游标分页"或"基于索引的分页"优化深分页查询。例如:
sql 复制代码
- - 普通分页(性能差)
SELECT * FROM user LIMIT 10000, 10;

- - 优化后的分页(假设id是主键且有序)
SELECT * FROM user WHERE id > 10000 ORDER BY id LIMIT 10;
  • 在MyBatis-Plus中,可以通过自定义SQL实现:
java 复制代码
@Select("SELECT * FROM user WHERE id > #{lastId} ORDER BY id LIMIT #{size}")
List<User> selectByPage(@Param("lastId") Long lastId, @Param("size") Integer size);

2.3 分页与排序的冲突

  • 问题描述*: 当分页与排序结合时,如果排序字段不是唯一或稳定的,可能会导致分页结果重复或丢失。例如:
java 复制代码
Pageable pageable = PageRequest.of(0, 10, Sort.by("name"));
Page<User> page = userRepository.findAll(pageable);

如果name字段有重复值,翻页时可能会出现重复数据。

  • 坑点分析*:
  • 分页的稳定性依赖于排序字段的唯一性。如果排序字段不唯一,数据库可能会以任意顺序返回重复值,导致分页结果不一致。
  • 解决方案*:
  • 在排序中添加唯一字段(如主键)作为次要排序条件:
java 复制代码
Sort sort = Sort.by("name").and(Sort.by("id"));
Pageable pageable = PageRequest.of(0, 10, sort);

2.4 分页结果的总数查询问题

  • 问题描述*: Spring Data JPA和MyBatis-Plus的分页查询通常包含两部分:
  1. 查询分页数据。
  2. 查询总数(用于计算总页数)。

在数据量大的表中,查询总数可能会非常耗时。

  • 坑点分析*:
  • 如果只需要分页数据而不需要总数(例如移动端上拉加载更多),默认的分页查询仍然会执行COUNT(*),造成性能浪费。
  • 解决方案*:
  • 在Spring Data JPA中,可以使用Slice代替Page,避免总数查询:
java 复制代码
Slice<User> slice = userRepository.findAll(pageable);
  • 在MyBatis-Plus中,可以通过page.setSearchCount(false)禁用总数查询:
java 复制代码
Page<User> page = new Page<>(1, 10);
page.setSearchCount(false);
userMapper.selectPage(page, null);

2.5 分页与一对多关联查询的陷阱

  • 问题描述 *: 在涉及一对多关联查询时(例如User和Order),分页可能会出现数据混乱或分页不准确的问题。

  • 坑点分析*:

  • 如果使用JPA的@OneToMany关联查询,分页是在内存中进行的,可能导致性能问题和数据不一致。
  • 在MyBatis中,如果关联查询的结果映射为嵌套对象,分页的LIMIT可能无法正确截断数据。
  • 解决方案*:
  • 避免在JPA中直接对一对多关联进行分页,改为使用子查询或DTO投影。
  • 在MyBatis中,使用单独的查询分页,再通过二次查询填充关联数据。

3. 我的踩坑经历

在我的项目中,使用的是Spring Data JPA的分页功能。前端传递的分页参数是page=1和size=10,但查询结果始终为空。经过排查,发现以下问题:

  1. 前端传递的page是从1开始的,而Spring Data JPA的PageRequest.of(page, size)是从0开始的。
  2. 分页查询的排序字段是非唯一的,导致翻页时数据重复。
  3. 关联查询的数据量很大,分页性能极差。

最终,我通过以下方式解决了问题:

  • 将前端的page减1传递给后端。
  • 在排序中添加主键字段。
  • 使用Slice代替Page避免总数查询。

总结

分页功能虽然看似简单,但在实际开发中隐藏了许多坑。从参数传递、性能优化到关联查询,每一个环节都需要仔细处理。通过本文的分析和解决方案,希望能帮助大家在SpringBoot分页开发中少走弯路。如果你也遇到过类似的分页问题,欢迎在评论区分享你的经验!

相关推荐
前进前进前进6 小时前
TRAE 全链路实战:做了一个警示牌一键生成器:纯 Canvas、单文件、零依赖
前端
武子康6 小时前
150毫秒和2400毫秒:同一 Agent 任务的两种延迟
人工智能·后端·agent
暖心hebbie设计学习6 小时前
GitHub 热榜周报(10.19)|5 个把 AI 装回本地的开源项目:多 agent 控制台、本地语音、节点式生成(含技术看点)
人工智能·github
猪脚踏浪6 小时前
open webui 在docker容器里跑,并且是在内网断绝互联网的环境下。生成ppt
人工智能
海边捡石子6 小时前
FreeSWITCH 入门:把电话系统接入语音客服机器人
人工智能·后端
桃西西呀7 小时前
我把网关限流配置成每秒 100 请求,大促一来照样被打挂,还把别人也拖垮了
人工智能·llm·ai编程
武子康7 小时前
Agent 偏好记忆怎样确认写入:范围、版本与撤销的一个最小例
人工智能·后端·agent
小万7427 小时前
God‘s Eye View 深度解析:15 路公开信号 + CesiumJS 的浏览器级 OSINT 控制台,60 帧渲染的“真实间谍卫星模拟器”
前端
IT星宿7 小时前
浏览器标签页全是一个图标:我用名字哈希生成 favicon,让每个项目都有身份标识
前端
Bazingga7 小时前
Java16 record 完整拆解——语法、语义、限制、反射
java·后端