- 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的分页查询通常包含两部分:
- 查询分页数据。
- 查询总数(用于计算总页数)。
在数据量大的表中,查询总数可能会非常耗时。
- 坑点分析*:
- 如果只需要分页数据而不需要总数(例如移动端上拉加载更多),默认的分页查询仍然会执行
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,但查询结果始终为空。经过排查,发现以下问题:
- 前端传递的
page是从1开始的,而Spring Data JPA的PageRequest.of(page, size)是从0开始的。 - 分页查询的排序字段是非唯一的,导致翻页时数据重复。
- 关联查询的数据量很大,分页性能极差。
最终,我通过以下方式解决了问题:
- 将前端的
page减1传递给后端。 - 在排序中添加主键字段。
- 使用
Slice代替Page避免总数查询。
总结
分页功能虽然看似简单,但在实际开发中隐藏了许多坑。从参数传递、性能优化到关联查询,每一个环节都需要仔细处理。通过本文的分析和解决方案,希望能帮助大家在SpringBoot分页开发中少走弯路。如果你也遇到过类似的分页问题,欢迎在评论区分享你的经验!