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查询问题:
- 第一阶段:复用原有过滤、排序逻辑,只分页查询符合条件的Product主键ID,这一步不关联任何集合表,保证数据库层面分页的准确性
- 第二阶段:根据第一阶段查出的Product ID列表,一次性通过fetch join加载所有关联的Variant、Images、Attribute数据,去重后组装成完整的Product对象
- 最后将组装好的完整数据、分页参数、总条数封装为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
相关产品推荐
相关产品推荐

