You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot 3.0.6分页返回总页数异常问题求助

问题:Spring Boot + JPA EntityGraph 分页异常(部分页码总页数错误)

场景复现

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 03:47:22