JPA Criteria API实现带过滤条件的一对多join查询
JPA Criteria API 实现一对多关联条件过滤查询方案
实体类定义
首先按照表结构定义两个JPA实体,配置一对多关联关系:
- Enterprise实体对应企业表,和Product是一对多关系
- Product实体对应产品表,用枚举标识产品状态
// 产品状态枚举 public enum ProductStatus { ACTIVE, DISABLE } @Entity @Table(name = "enterprise") public class Enterprise { @Id private Long id; private String name; // 关联配置为懒加载,不做全局过滤,查询时动态控制关联条件 @OneToMany(mappedBy = "enterprise", fetch = FetchType.LAZY) private List<Product> products; // 省略getter、setter } @Entity @Table(name = "product") public class Product { @Id private Long id; private String name; @Enumerated(EnumType.STRING) private ProductStatus status; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "enterprise_id") private Enterprise enterprise; // 省略getter、setter }
核心查询逻辑实现
使用Criteria API编写查询时,关键是用fetch join加载关联数据,同时把产品状态的过滤条件写在关联的on子句上,而不是全局where子句上,避免把无有效产品的企业整条过滤掉。
@Repository public class EnterpriseRepository { @PersistenceContext private EntityManager entityManager; public List<Enterprise> queryEnterprisesWithActiveProducts() { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Enterprise> cq = cb.createQuery(Enterprise.class); Root<Enterprise> enterpriseRoot = cq.from(Enterprise.class); // 左连接fetch抓取产品集合,保证没有有效产品的企业也能被查询到 Join<Enterprise, Product> productJoin = (Join<Enterprise, Product>) enterpriseRoot .fetch("products", JoinType.LEFT); // 仅在关联时加载状态为ACTIVE的产品,条件写在on上而非where productJoin.on(cb.equal(productJoin.get("status"), ProductStatus.ACTIVE)); // 加distinct去重,避免一对多关联查询产生重复的企业实体 cq.select(enterpriseRoot).distinct(true); return entityManager.createQuery(cq).getResultList(); } }
注意:如果项目开启了JPA元模型生成,也可以用元模型属性(
Enterprise_.products、Product_.status)替代字符串属性名,实现类型安全的属性访问,效果一致。
结果序列化处理
查询得到的Enterprise实体中,products集合已经只包含状态为ACTIVE的产品,直接做JSON序列化即可得到要求的结构。如果需要严格控制返回字段,也可以转换为DTO输出:
// 返回结构DTO public record EnterpriseVO(Long id, String name, List<ProductVO> products) {} public record ProductVO(Long id, String name) {} // 结果转换 List<Enterprise> enterpriseList = enterpriseRepository.queryEnterprisesWithActiveProducts(); List<EnterpriseVO> result = enterpriseList.stream() .map(ent -> new EnterpriseVO( ent.getId(), ent.getName(), ent.getProducts().stream() .map(prod -> new ProductVO(prod.getId(), prod.getName())) .toList() )).toList();
针对题目给出的示例数据,最终输出的JSON结构和要求完全一致:ID=1的企业Alex关联的产品集合仅包含Iphone12、Iphone11两个ACTIVE状态的条目,DISABLE状态的Iphone13不会出现在结果中。
常见避坑说明
- 不要在实体的
@OneToMany关联上加@Where(clause = "status = 'ACTIVE'")做全局过滤:这种方式会导致所有查询企业的场景都自动过滤非激活产品,灵活性极差 - 不要先查所有企业再循环遍历查询每个企业的激活产品:会产生N+1查询问题,数据量大时性能严重下降
- 不要把产品状态过滤条件写在where子句中:会导致没有激活产品的企业被整条过滤,不符合业务预期
- 不要遗漏
distinct(true)配置:一对多fetch join会生成笛卡尔积结果,同一个企业会对应多行产品记录,不加去重会返回重复的企业对象
内容的提问来源于stack exchange,提问作者Son Nguyen
相关产品推荐
相关产品推荐

