Spring Boot 3.0.6分页返回总页数异常问题求助
场景复现
使用Spring Boot Starter Parent 3.0.6开发卡牌查询接口,通过Pageable实现分页,核心代码如下:
控制器代码
@RestController @RequestMapping("/api/cards") @RequiredArgsConstructor public class CardsController { private final BerserkHeroesCardService cardService; @GetMapping @ResponseBody public Page<BerserkCardModel> getCards(CardSearchRequest searchRequest, Pageable pageable) { return cardService.getCards(searchRequest, pageable); } }
服务层代码
@Override public Page<BerserkCardModel> getCards(CardSearchRequest request, Pageable pageable) { Specification<BerserkCard> spec = Specification.where(null); // 调试用空Specification Page<BerserkCard> all = berserkCardRepo.findAll(spec, pageable); // page2、3返回错误结果 return all.map(mapper::toModel); }
Repository代码(带问题注解)
public interface BerserkCardRepo extends JpaRepository<BerserkCard, Long>, JpaSpecificationExecutor<BerserkCard> { @Override @EntityGraph(attributePaths = {"elements"}) Page<BerserkCard> findAll(@Nullable Specification<BerserkCard> spec, Pageable pageable); }
实体类核心定义
@Data @Entity @Table(name = "berserk_cards") public class BerserkCard { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 基础字段... @ElementCollection @CollectionTable(name = "berserk_card_elements", joinColumns = @JoinColumn(name = "card_id")) @Column(name = "element") private Set<String> elements = new HashSet<>(); // 其他字段... }
异常现象
- 调用
/api/cards?page=1&size=12:返回正确结果,totalElements=2726,totalPages=228 - 调用
/api/cards?page=2&size=12:返回错误结果,totalElements=23,totalPages=2,last=true - 调用
/api/cards?page=3&size=12:返回错误结果,totalElements=35,totalPages=3,last=true - 调用
/api/cards?page=4&size=12:恢复正确结果,与page1一致
关键观察:启用@EntityGraph时,page2、page3不会执行count查询,仅执行数据查询;禁用该注解后,所有页码都会执行count查询,结果正常。
异常原因
这是Hibernate处理关联集合(@ElementCollection)的EntityGraph分页时的查询优化逻辑bug:
当使用@EntityGraph加载elements集合时,Hibernate会通过左连接主表与关联表的方式查询数据,这会因为关联集合的元素数量导致主表数据被重复返回(比如一个卡片有3个元素,会返回3条重复的卡片记录)。
此时Hibernate会根据当前查询返回的总行数,与pageNumber * pageSize进行比较:
- 如果总行数 <
pageNumber * pageSize,Hibernate会错误判定已经到达最后一页,跳过count查询,直接用当前查询的总行数作为totalElements,进而计算出错误的totalPages。 - 例如page=2&size=12时,左连接后的总行数是23,212=24,23<24,所以触发错误逻辑;page=3时312=36,左连接行数35<36,同样触发错误;而page1、page4的左连接行数大于对应阈值,会正常执行count查询。
解决方法
方案1:手动拆分count查询与分页查询
在服务层手动执行count查询,再单独获取分页数据,最后组装成Page对象,绕过Hibernate的错误优化:
@Override public Page<BerserkCardModel> getCards(CardSearchRequest request, Pageable pageable) { Specification<BerserkCard> spec = Specification.where(null); // 手动执行count查询,获取正确的总数据量 long totalElements = berserkCardRepo.count(spec); // 获取当前页的内容 List<BerserkCard> content = berserkCardRepo.findAll(spec, pageable).getContent(); // 手动组装Page对象 Page<BerserkCard> page = new PageImpl<>(content, pageable, totalElements); return page.map(mapper::toModel); }
方案2:用Specification的fetch join替代EntityGraph
去掉Repository上的@EntityGraph注解,在Specification中通过fetch join加载关联集合,同时设置distinct避免重复行:
@Override public Page<BerserkCardModel> getCards(CardSearchRequest request, Pageable pageable) { Specification<BerserkCard> spec = (root, query, cb) -> { // 显式指定fetch join加载elements集合,同时设置distinct避免重复行 root.fetch("elements", JoinType.LEFT); query.distinct(true); return cb.conjunction(); }; Page<BerserkCard> all = berserkCardRepo.findAll(spec, pageable); return all.map(mapper::toModel); }
方案3:升级Hibernate/Spring Boot版本
该bug在Hibernate 6.2+版本中已被修复,Spring Boot 3.1.x及以上版本默认使用Hibernate 6.2+。可以升级Spring Boot到3.1.x或更高版本,直接解决该问题。
方案4:禁用EntityGraph(简易方案)
如果对性能影响不大,直接去掉Repository上的@EntityGraph注解,让elements集合默认延迟加载。但需注意避免N+1查询问题,可在需要批量加载时结合fetch join使用。
内容的提问来源于stack exchange,提问作者Baneslayer55

