Spring Boot中基于JPA Criteria Query实现重叠路径表达式过滤的问题
解决JPA Criteria Query处理自关联实体的重叠路径过滤问题
嘿,我来帮你搞定这个JPA Criteria Query处理重叠路径过滤的问题!首先咱们先把基础的实体配置补全(你给的代码里缺了关键的JPA注解,不然框架没法正确识别关联关系),再一步步拆解不同场景的实现方式~
补全后的实体代码
import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; @Entity class Customer { @Id private Long id; private String name; // 补充@ManyToOne注解,建立Customer到Company的关联 @ManyToOne(fetch = FetchType.LAZY) private Company company; // 省略getter/setter } @Entity class Company { @Id private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ownedBy") private Company ownedBy; // 省略getter/setter }
你提到的重叠路径表达式,本质是自关联实体(Company)的链式路径(比如company.ownedBy、company.ownedBy.ownedBy这类重复指向自身的路径)。下面分两种常见场景给出实现方案:
场景1:固定层数的重叠路径过滤(比如直接持有)
比如需求是:查询所有属于被指定公司直接持有(即customer.company.ownedBy.id = 指定ID)的客户。
用Criteria API实现的代码如下:
// 在Spring Boot中可通过@Autowired注入EntityManager @Autowired private EntityManager entityManager; public List<Customer> findCustomersByDirectlyOwnedCompany(Long ownerCompanyId) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Customer> query = cb.createQuery(Customer.class); // 从Customer表作为查询根 Root<Customer> customerRoot = query.from(Customer.class); // 构建关联路径:Customer -> Company -> ownedBy(另一个Company) Join<Customer, Company> companyJoin = customerRoot.join("company"); Join<Company, Company> ownedByJoin = companyJoin.join("ownedBy"); // 添加过滤条件:ownedBy的ID等于指定值 query.where(cb.equal(ownedByJoin.get("id"), ownerCompanyId)); return entityManager.createQuery(query).getResultList(); }
如果想避免字符串硬编码(防止字段名拼写错误),可以用JPA元模型(Metamodel),生成Customer_和Company_类后,代码可以改成更安全的版本:
Join<Customer, Company> companyJoin = customerRoot.join(Customer_.company); Join<Company, Company> ownedByJoin = companyJoin.join(Company_.ownedBy); query.where(cb.equal(ownedByJoin.get(Company_.id), ownerCompanyId));
场景2:递归/多层重叠路径过滤(比如间接持有)
如果需求是查询所有属于被指定公司直接或间接持有(任意层数)的公司的客户,这时候需要用JPA 2.1引入的**递归公共表表达式(CTE)**来实现,Criteria API支持这种写法:
public List<Customer> findCustomersByIndirectlyOwnedCompany(Long rootOwnerId) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Customer> mainQuery = cb.createQuery(Customer.class); // 1. 定义递归CTE:找出所有被rootOwnerId直接/间接持有的公司 CommonTableExpression<Company> recursiveCompanies = cb.createCommonTableExpression(Company.class, "recursive_companies"); // 1.1 初始查询:找出被rootOwnerId直接持有的公司 CriteriaQuery<Company> initialQuery = cb.createQuery(Company.class); Root<Company> initialRoot = initialQuery.from(Company.class); initialQuery.select(initialRoot) .where(cb.equal(initialRoot.get("ownedBy").get("id"), rootOwnerId)); // 1.2 递归查询:找出被CTE中已有的公司持有的公司 CriteriaQuery<Company> recursiveQuery = cb.createQuery(Company.class); Root<Company> recursiveRoot = recursiveQuery.from(Company.class); Join<Company, Company> recursiveJoin = recursiveRoot.join("ownedBy"); recursiveQuery.select(recursiveRoot) .where(cb.equal(recursiveJoin.get("id"), recursiveCompanies.get("id"))); // 1.3 配置CTE为UNION ALL模式(递归) recursiveCompanies.setUnionAll(true); recursiveCompanies.setInitialQuery(initialQuery); recursiveCompanies.setRecursiveQuery(recursiveQuery); // 2. 主查询:关联Customer和CTE中的公司 Root<Customer> customerRoot = mainQuery.from(Customer.class); // 子查询:获取所有符合条件的公司ID Subquery<Long> companyIdSubquery = mainQuery.subquery(Long.class); Root<Company> subqueryRoot = companyIdSubquery.from(recursiveCompanies); companyIdSubquery.select(subqueryRoot.get("id")); // 添加过滤条件:Customer的公司ID在子查询结果中 mainQuery.where(cb.in(customerRoot.get("company").get("id")).value(companyIdSubquery)); // 将CTE添加到主查询中 mainQuery.with(recursiveCompanies); return entityManager.createQuery(mainQuery).getResultList(); }
关键注意点
- 重叠路径的核心是通过多次join或Path.get()链式调用构建自关联路径,固定层数直接链式调用即可,动态层数则用递归CTE。
- 懒加载(FetchType.LAZY)不会影响Criteria Query的执行,因为join操作会触发关联实体的加载。
- 尽量使用JPA元模型来构建路径,避免字符串硬编码带来的维护问题。
内容的提问来源于stack exchange,提问作者mhuelfen
相关产品推荐
相关产品推荐

