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

JPA CriteriaQuery实现多表关联带过滤排序分页查询

数据库表结构

数据库共包含4张业务表,结构如下:

  • Product:主键为productid
  • Variant:主键为variantid,外键productid关联Product表
  • Images:主键为imageid,外键variantid关联Variant表
  • Attribute:主键为attributid,外键variantid关联Variant表

实体关联关系:单个Product对应多个Variant,单个Variant对应多个Attribute、多个Images。

实现目标

Java REST API 需要实现两个核心能力:

  • 支持带过滤、排序条件的分页查询
  • 查询返回的Product对象需嵌套其关联的所有Variant数据,每个Variant需嵌套其关联的Images、Attribute集合
现有代码

目前已经实现基于JPA Criteria的Product单表分页查询,单表场景下分页运行正常,代码如下:

@Repository
public class PaginProductCriteriaRepository {

    private final EntityManager entityManager;
    private final CriteriaBuilder criteriaBuilder;


    public PaginProductCriteriaRepository(EntityManager entityManager) {
        this.entityManager = entityManager;
        this.criteriaBuilder = entityManager.getCriteriaBuilder();
    }

    public Page<Product> findAllWithFilter(PaginProductPage paginPage, PaginProductSearchCriteria paginSearchCriteria){
        
        CriteriaQuery<Product> productCriteriaQuery = criteriaBuilder.createQuery(Product.class); 
        Root<Product> rootProduct = productCriteriaQuery.from(Product.class); 
         
        Predicate predicate = getPrediate(paginSearchCriteria,rootProduct); 
        productCriteriaQuery.where(predicate);
        setOrder(paginPage,productCriteriaQuery,rootProduct);
        
        TypedQuery<Product> typedQuery = entityManager.createQuery(productCriteriaQuery);
        typedQuery.setFirstResult(paginPage.getPageNumber() * paginPage.getPageSize());
        typedQuery.setMaxResults(paginPage.getPageSize());
        
        Pageable pageable = getPageable(paginPage);
        
        long paginCount = getPaginCountMethod(predicate);
        
        return new PageImpl<>(typedQuery.getResultList(),pageable,paginCount);
    }


    private Predicate getPrediate(PaginProductSearchCriteria paginSearchCriteria, Root<Product> root) {
        List<Predicate>  predicates = new ArrayList<>();
        
        if (Objects.nonNull(paginSearchCriteria.getpName())) {
            predicates.add(criteriaBuilder.like(root.get("pName"),"%" + paginSearchCriteria.getpName() + "%"));
        }
        return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
    }

    private void setOrder(PaginProductPage paginPage, CriteriaQuery<Product> criteriaQuery, Root<Product> root) {
        if (paginPage.getSortDirection().equals(Sort.Direction.ASC)) {
        criteriaQuery.orderBy(criteriaBuilder.asc(root.get(paginPage.getSortBy())));
        } else {
        criteriaQuery.orderBy(criteriaBuilder.desc(root.get(paginPage.getSortBy())));
        }
    }


    private Pageable getPageable(PaginProductPage paginPage) {
         Sort sort  = Sort.by(paginPage.getSortDirection(),paginPage.getSortBy());
         return PageRequest.of(paginPage.getPageNumber(),paginPage.getPageSize(),sort);
    }

    private long getPaginCountMethod(Predicate predicate) {
        CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);
        Root<Product> countRoot =  countQuery.from(Product.class);
        countQuery.select(criteriaBuilder.count(countRoot)).where(predicate);
        return entityManager.createQuery(countQuery).getSingleResult();
    }

}
改造实现方案

直接在分页查询中使用fetch join关联多层集合会导致两个问题:一是连接一对多集合时产生重复行,导致分页计数不准;二是Hibernate会对集合关联的查询执行内存分页,性能极差。因此采用两阶段查询的方案改造,既保证分页准确性,又避免N+1查询问题:

  1. 第一阶段:复用原有过滤、排序逻辑,只分页查询符合条件的Product主键ID,这一步不关联任何集合表,保证数据库层面分页的准确性
  2. 第二阶段:根据第一阶段查出的Product ID列表,一次性通过fetch join加载所有关联的Variant、Images、Attribute数据,去重后组装成完整的Product对象
  3. 最后将组装好的完整数据、分页参数、总条数封装为Page对象返回

改造后的完整代码如下:

@Repository
public class PaginProductCriteriaRepository {

    private final EntityManager entityManager;
    private final CriteriaBuilder criteriaBuilder;


    public PaginProductCriteriaRepository(EntityManager entityManager) {
        this.entityManager = entityManager;
        this.criteriaBuilder = entityManager.getCriteriaBuilder();
    }

    public Page<Product> findAllWithFilter(PaginProductPage paginPage, PaginProductSearchCriteria paginSearchCriteria){
        // 1. 构造通用查询条件
        CriteriaQuery<Product> productCriteriaQuery = criteriaBuilder.createQuery(Product.class);
        Root<Product> rootProduct = productCriteriaQuery.from(Product.class);
        Predicate predicate = getPrediate(paginSearchCriteria,rootProduct);

        // 2. 第一阶段:分页查询符合条件的Product ID,保证分页逻辑在数据库层执行
        CriteriaQuery<Long> idQuery = criteriaBuilder.createQuery(Long.class);
        Root<Product> idRoot = idQuery.from(Product.class);
        idQuery.select(idRoot.get("productid")).where(predicate);
        setOrder(paginPage, idQuery, idRoot);

        TypedQuery<Long> idTypedQuery = entityManager.createQuery(idQuery);
        idTypedQuery.setFirstResult(paginPage.getPageNumber() * paginPage.getPageSize());
        idTypedQuery.setMaxResults(paginPage.getPageSize());
        List<Long> productIds = idTypedQuery.getResultList();

        // 无匹配数据直接返回空分页
        if (productIds.isEmpty()) {
            return new PageImpl<>(Collections.emptyList(), getPageable(paginPage), 0);
        }

        // 3. 第二阶段:根据ID批量fetch关联所有嵌套数据,杜绝N+1查询
        CriteriaQuery<Product> fetchQuery = criteriaBuilder.createQuery(Product.class);
        Root<Product> fetchRoot = fetchQuery.from(Product.class);
        // 左连接关联Variant集合
        Fetch<Product, Variant> variantFetch = fetchRoot.fetch("variants", JoinType.LEFT);
        // 左连接关联Variant下的Images和Attribute集合
        variantFetch.fetch("images", JoinType.LEFT);
        variantFetch.fetch("attributes", JoinType.LEFT);
        // 加ID范围过滤
        fetchQuery.where(fetchRoot.get("productid").in(productIds)).distinct(true);
        // 保持和分页查询一致的排序规则
        setOrder(paginPage, fetchQuery, fetchRoot);

        List<Product> fullProducts = entityManager.createQuery(fetchQuery).getResultList();

        // 4. 查询总条数组装分页结果
        long paginCount = getPaginCountMethod(predicate);
        return new PageImpl<>(fullProducts, getPageable(paginPage), paginCount);
    }


    private Predicate getPrediate(PaginProductSearchCriteria paginSearchCriteria, Root<Product> root) {
        List<Predicate>  predicates = new ArrayList<>();
        if (Objects.nonNull(paginSearchCriteria.getpName())) {
            predicates.add(criteriaBuilder.like(root.get("pName"),"%" + paginSearchCriteria.getpName() + "%"));
        }
        return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
    }

    // 改造排序方法支持泛型,同时适配ID查询和实体查询
    private <T> void setOrder(PaginProductPage paginPage, CriteriaQuery<T> criteriaQuery, Root<T> root) {
        if (paginPage.getSortDirection().equals(Sort.Direction.ASC)) {
            criteriaQuery.orderBy(criteriaBuilder.asc(root.get(paginPage.getSortBy())));
        } else {
            criteriaQuery.orderBy(criteriaBuilder.desc(root.get(paginPage.getSortBy())));
        }
    }


    private Pageable getPageable(PaginProductPage paginPage) {
         Sort sort  = Sort.by(paginPage.getSortDirection(),paginPage.getSortBy());
         return PageRequest.of(paginPage.getPageNumber(),paginPage.getPageSize(),sort);
    }

    private long getPaginCountMethod(Predicate predicate) {
        CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);
        Root<Product> countRoot =  countQuery.from(Product.class);
        countQuery.select(criteriaBuilder.count(countRoot)).where(predicate);
        return entityManager.createQuery(countQuery).getSingleResult();
    }

}

注意事项

  • 代码中variants、images、attributes为实体类中定义的关联属性名,需要和实际实体类中@OneToMany注解标注的属性名保持一致
  • 所有关联使用左连接,保证没有挂载Variant/Images/Attribute的Product也能被正常查询出来
  • 第二阶段查询加distinct(true)是为了去除join多层集合产生的重复Product对象

内容的提问来源于stack exchange,提问作者kirti

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 10:01:48