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 分页参数传递问题

  • 问题描述 *: 在前端传递分页参数时,通常是通过pagesize两个参数。然而,如果前端传递的参数值为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, sizeoffset较大时性能极差。
  • 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 分页与一对多关联查询的陷阱

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

  • 坑点分析*:

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

3. 我的踩坑经历

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

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

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

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

总结

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

相关推荐
颜酱1 小时前
05 | 召回前置准备:根据业务数据库生成各数据库(读取配置阶段)
前端·人工智能·后端
Wang's Blog1 小时前
Go-Zero项目开发24: 基于Bitmap实现群聊消息已读未读
开发语言·后端·golang
Infedium2 小时前
英特物理AI仿真赋能制造!打破传统有限元瓶颈,研发提效降本翻倍
人工智能·制造
zandy10112 小时前
衡石 Agentic BI的ReAct 推理框架在 Agentic BI 中的工程化实践
前端·javascript·react.js
罗超驿2 小时前
JavaEE进阶之路:从Web架构原理到HTML标签全解析
前端·html·web·javaee
天国梦2 小时前
2026英语教学系统选型实战:AI如何让备课效率提升42%?天学网技术落地全解析
人工智能·学习
西安小哥2 小时前
从前端到AI工程师:一场跨越鸿沟的真实蜕变之旅
前端
Prince4182 小时前
侧边栏收起缩放适配方案
前端
Conan在掘金2 小时前
鸿蒙报错速查:struct 里嵌 namespace 声明就炸,根因 + 真解法
后端