
本教程详细阐述了如何使用JPA Criteria API进行复杂的数据库查询,特别是涉及多层关联实体(如一对多关系)的路径导航,并根据关联实体中某个属性的值是否包含在给定列表中进行过滤。文章通过具体的实体模型和代码示例,指导开发者构建动态、类型安全的查询,避免常见的错误,并强调了`Join`操作和`in`谓词的正确使用方法。
在现代Java持久化应用中,JPA Criteria API提供了一种类型安全、编程化的方式来构建动态查询,替代了传统的JPQL字符串。它尤其适用于那些查询条件不固定,需要根据运行时参数灵活组合的场景。本教程将聚焦于一个常见但稍显复杂的查询需求:如何通过Criteria API导航到多层关联实体,并根据关联实体集合中某个属性的值是否在给定列表中来筛选主实体。
实体模型概览
为了更好地说明问题,我们首先定义一组相互关联的实体。假设我们有一个Property实体,它与Amenities实体是一对一关系,而Amenities实体又与Interiors实体是一对多关系。Interiors实体包含一个name属性。
// Property Entity
@Entity
public class Property {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String address;
@OneToOne(mappedBy = "property", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private Amenities amenities;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public Amenities getAmenities() { return amenities; }
public void setAmenities(Amenities amenities) { this.amenities = amenities; }
}
// Amenities Entity
@Entity
public class Amenities {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "property_id")
@JsonBackReference
private Property property;
@OneToMany(mappedBy = "amenities", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List interiors = new ArrayList<>();
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Property getProperty() { return property; }
public void setProperty(Property property) { this.property = property; }
public List getInteriors() { return interiors; }
public void setInteriors(List interiors) { this.interiors = interiors; }
public void addInterior(Interiors interior) {
this.interiors.add(interior);
interior.setAmenities(this);
}
public void removeInterior(Interiors interior) {
this.interiors.remove(interior);
interior.setAmenities(null);
}
}
// Interiors Entity
@Entity
public class Interiors {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name; // e.g., "Gym", "Pool", "Sauna"
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "amenities_id")
@JsonBackReference
private Amenities amenities;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Amenities getAmenities() { return amenities; }
public void setAmenities(Amenities amenities) { this.amenities = amenities; }
} 我们的目标是查询所有包含特定内部设施(例如,名称为“Gym”或“Pool”)的Property实体。
Criteria API 路径导航与in谓词
在Criteria API中,当我们进行查询时,通常从Root对象开始,它代表了查询的主实体。通过join()方法,我们可以导航到关联实体。对于一对一或多对一关系,join()会返回一个Join对象。对于一对多或多对多关系,join()方法也会返回一个Join对象,代表了集合中的元素。
考虑以下查询场景:查找所有拥有“Gym”或“Pool”设施的房产。
错误的尝试与分析
初学者可能会尝试如下代码:
// 假设 propertyRoot 是 Root// criteriaBuilder.equal(propertyRoot.join("amenities").join("interiors"). get("name"), "Gym");
这段代码的问题在于,propertyRoot.join("amenities").join("interiors")会尝试导航到Interiors集合中的一个元素,并期望其name属性等于"Gym"。然而,join("interiors")返回的是一个Join
正确的做法是明确地进行连接操作,并使用in谓词来检查关联实体属性是否在指定值列表中。
正确的查询构建方法
要实现“查找所有拥有指定名称的内部设施的房产”这一目标,我们需要执行以下步骤:
- 获取CriteriaBuilder和CriteriaQuery实例。
- 定义查询的Root,即从哪个实体开始查询(Property)。
- 通过join()方法导航到Amenities实体。
- 再次通过join()方法导航到Interiors集合。
- 使用CriteriaBuilder的in()方法创建一个Predicate,检查Interiors的name属性是否在给定的值列表中。
- 将这个Predicate应用到CriteriaQuery的where()子句中。
以下是实现上述逻辑的完整代码示例:
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Predicate;
import java.util.Arrays;
import java.util.List;
// 假设在一个Service或Repository类中
public class PropertyService {
@PersistenceContext
private EntityManager entityManager;
public List findPropertiesWithSpecificInteriors(List interiorNames) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(Property.class);
Root propertyRoot = query.from(Property.class);
// 1. 导航到 Amenities
// Join amenitiesJoin = propertyRoot.join("amenities");
// 注意:如果 amenities 可能是 null,可以使用 JoinType.LEFT
Join amenitiesJoin = propertyRoot.join("amenities");
// 2. 导航到 Interiors 集合
// Join interiorsJoin = amenitiesJoin.join("interiors");
// 同样,如果 interiors 集合可能为空,并且你仍想返回 Property(即使没有匹配的 interiors),可以使用 JoinType.LEFT
Join interiorsJoin = amenitiesJoin.join("interiors");
// 3. 创建 Predicate: interiorsJoin.get("name") in (interiorNames)
Predicate nameInListPredicate = interiorsJoin.get("name").in(interiorNames);
// 4. 将 Predicate 应用到 where 子句
query.where(nameInListPredicate);
// 5. 执行查询并返回结果
TypedQuery typedQuery = entityManager.createQuery(query.distinct(true)); // distinct(true) 避免重复的 Property
return typedQuery.getResultList();
}
// 示例用法
public void demoQuery() {
List desiredInteriors = Arrays.asList("Gym", "Pool");
List properties = findPropertiesWithSpecificInteriors(desiredInteriors);
System.out.println("Properties with Gym or Pool:");
for (Property p : properties) {
System.out.println("Property ID: " + p.getId() + ", Address: " + p.getAddress());
if (p.getAmenities() != null) {
p.getAmenities().getInteriors().forEach(i -> System.out.println(" - Interior: " + i.getName()));
}
}
}
} 代码解析与注意事项
-
Root
propertyRoot = query.from(Property.class); : 这行代码定义了查询的起始点是Property实体。 -
Join
amenitiesJoin = propertyRoot.join("amenities"); : 通过propertyRoot对象,我们调用join("amenities")来导航到Property的amenities属性。这里会创建一个内部连接(INNER JOIN)。如果Property可能没有Amenities但你仍想查询这些Property(并且只在有Amenities时才考虑Interiors),你可以使用propertyRoot.join("amenities", JoinType.LEFT)。 -
Join
interiorsJoin = amenitiesJoin.join("interiors"); : 类似地,从amenitiesJoin对象,我们导航到Amenities的interiors集合。这也会创建一个内部连接。 -
Predicate nameInListPredicate = interiorsJoin.get("name").in(interiorNames);: 这是核心部分。
- interiorsJoin.get("name"):获取Interiors实体中name属性的Path表达式。
- .in(interiorNames):Path对象的in()方法用于创建一个Predicate,判断该Path表达式的值是否包含在interiorNames列表中。
- query.where(nameInListPredicate);: 将生成的谓词应用到查询的WHERE子句中。
- query.distinct(true): 由于Property与Interiors之间存在一对多关系(通过Amenities),一个Property可能包含多个满足条件的Interiors。如果不使用distinct(true),查询结果中可能会出现重复的Property实体。distinct(true)会确保返回的Property实体是唯一的。
总结
通过JPA Criteria API,我们可以以类型安全的方式构建复杂的查询,包括多层关联实体的路径导航和基于列表的条件过滤。关键在于理解Root、Join对象的作用,以及如何利用Path对象的in()方法来构建IN谓词。正确使用JoinType(如INNER或LEFT)和distinct(true)可以帮助我们更精确地控制查询结果,避免不必要的重复数据和潜在的NullPointerException。掌握这些技巧将极大地提升您在Java持久化应用中处理复杂查询的能力。










