使用 JPA Criteria 連接無關係表
1. 概述
在 JPA 中,我們使用諸如@OneToMany之類的關係註解來定義兩個實體之間的關聯。這些註解讓我們可以使用 JPQL 或 CriteriaBuilder 來連接它們。然而,由於遺留資料庫設計或效能要求等因素,這種關係並非總是清晰定義的。
本教學將探討如何建立 JPA 條件查詢來連接表,而無需明確定義 JPA 關係。
2. 資料準備
為了說明這個概念,我們準備兩個 JPA 實體類別: School和Student 。這兩個實體類別代表兩個資料庫表,它們之間存在一對多的關係,其中一所學校可以有多名學生:
@Entity
@Table(name = "school")
public class School {
@Id
@Column(name = "id")
private int id;
@Column(name = "name")
private String name;
@OneToMany
@JoinColumn(name = "school_id", referencedColumnName = "id")
private List students;
// constructors, setters and getters
}
@Entity
@Table(name = "student")
public class Student {
@Id
@Column(name = "id")
private int id;
@Column(name = "school_id")
private int schoolId;
@Column(name = "name")
private String name;
// constructors, setters and getters
}
在後續章節中,我們將設定EntityManagerFactory以啟用 JPA 屬性hibernate.show_sql和hibernate.format_sql.
這些設定使 Hibernate 能夠將產生的 SQL 列印到控制台日誌中。這使我們能夠了解我們的 Criteria 查詢在運行時是如何轉換為 SQL 的:
private EntityManagerFactory createEntityManagerFactory() {
return new PersistenceConfiguration("SchoolData")
.jtaDataSource("java:comp/env/jdbc/SchoolData")
.managedClass(School.class)
.managedClass(Student.class)
.property("hibernate.show_sql", true)
.property("hibernate.format_sql", true)
.createEntityManagerFactory();
}
3. 有連接的條件查詢
當在School實體類別上定義了一對多關係時,我們可以使用以下條件查詢來取得擁有名為 Benjamin Lee 的Student的School :
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<School> query = cb.createQuery(School.class);
Root<School> schoolRoot = query.from(School.class);
Join<School, Student> studentJoin = schoolRoot.join("students");
cq.select(schoolRoot)
.where(cb.equal(studentJoin.get("name"), "Benjamin Lee"));
List<School> schools = em.createQuery(query).getResultList();
這基本上將School實體定義為根實體,並根據我們的關係定義,透過Join類別將其連接到Student實體。我們可以在控制台中執行此 Criteria 查詢時找到產生的 SQL:
select
s1_0.id,
s1_0.name
from
school s1_0
join
student s2_0
on s1_0.id=s2_0.school_id
where
s2_0.name=?
但是, Join操作只有在定義了關係之後才能正常運作。讓我們移除關係定義,包括註解和School類別中的實例變數students ,看看會發生什麼:
@Entity
@Table(name = "school")
public class School {
@Id
@Column(name = "id")
private int id;
@Column(name = "name")
private String name;
// constructors, setters and getters
}
這次,我們運行程式時看到了拋出的例外:
org.hibernate.query.sqm.PathElementException: Could not resolve attribute 'students' of 'com.baeldung.criteria.School'
4. 帶子查詢的條件查詢
有多種方法可以消除關係定義。其中一個方法是透過SubQuery類別定義子查詢,根據目標學生的schoolId選擇School Student:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<School> query = cb.createQuery(School.class);
Root<School> schoolRoot = query.from(School.class);
Subquery<Long> subquery = query.subquery(Long.class);
Root<Student> studentRoot = subquery.from(Student.class);
subquery.select(studentRoot.get("schoolId"))
.where(cb.equal(studentRoot.get("name"), "Benjamin Lee"));
query.select(schoolRoot)
.where(schoolRoot.get("id").in(subquery));
List<School> schools = em.createQuery(query).getResultList();
在子查詢中,我們從名為 Benjamin Lee 的Student實體中選擇schoolId ,並根據此schoolId選擇School實體。
in(…)謂詞充當這兩個實體之間的橋樑,並透過它們的標識符將它們連接起來,而無需任何JPA關係。
我們可以看到 JPA 為對應的查詢條件產生了以下 SQL:
select
s1_0.id,
s1_0.name
from
school s1_0
where
s1_0.id in ((select
s2_0.school_id
from
student s2_0
where
s2_0.name=?))
5. 具有交叉連接的條件查詢
我們不妨問問自己,是否能用更自然的方式將這兩個實體連結起來。實際上,我們可以在條件查詢中定義一個交叉連結來實現相同的結果:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<School> query = cb.createQuery(School.class);
Root<School> schoolRoot = query.from(School.class);
Root<Student> studentRoot = query.from(Student.class);
Predicate joinCondition = cb.equal(schoolRoot.get("id"), studentRoot.get("schoolId"));
Predicate studentName = cb.equal(studentRoot.get("name"), "Benjamin Lee");
query.select(schoolRoot)
.where(cb.and(joinCondition, studentName));
List<School> schools = em.createQuery(query).getResultList();
關鍵在於定義兩個Root實例: School根和Student根。**透過明確定義多個根,JPA 將在對應的資料庫表之間建立交叉連接。**
執行後,我們可以從控制台日誌中看到條件查詢轉換為以下 SQL:
select
s1_0.id,
s1_0.name
from
school s1_0,
student s2_0
where
s1_0.id=s2_0.school_id
and s2_0.name=?
與編寫 SQL 查詢類似,我們應該明確地設定條件來限制傳回的記錄數。在我們的範例中,我們使用學生姓名來限制結果集。由於交叉連接會產生笛卡爾積,因此如果兩個表都包含大量資料行,則會傳回龐大的結果集。
6. 使用元組選擇進行條件查詢
在先前的交叉連接查詢中,我們只回傳了School實體。然而,這可能不夠,因為我們可能還需要檢索Student實體中的資料。
要在相同查詢中傳回Student實體,我們可以呼叫CriteriaBuilder的createTupleQuery()方法。這樣我們就可以選擇Tuple而不是School,因為 School 可以包含多個實體:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> query = cb.createTupleQuery();
Root<School> schoolRoot = query.from(School.class);
Root<Student> studentRoot = query.from(Student.class);
Predicate joinCondition = cb.equal(schoolRoot.get("id"), studentRoot.get("schoolId"));
Predicate studentName = cb.equal(studentRoot.get("name"), "Benjamin Lee");
query.select(cb.tuple(schoolRoot,studentRoot))
.where(cb.and(joinCondition, studentName));
List<Tuple> tuples = em.createQuery(query).getResultList();
元組查詢允許我們從CriteriaQuery中選擇多個實體。在本例中,我們同時取得了School和Student實體。
透過分析控制台日誌中產生的 SQL,我們發現 JPA 在 select 子句中包含了student表中的列,這表示它也會從Student實體檢索資料:
select
s1_0.id,
s1_0.name,
s2_0.id,
s2_0.name,
s2_0.school_id
from
school s1_0,
student s2_0
where
s1_0.id=s2_0.school_id
and s2_0.name=?
每個Tuple都會傳回一個結果行,其中包含按照我們在條件查詢中指定的順序選定的實體。要取得第一個結果行中的實體,我們可以簡單地:
Tuple firstTuplpe = tuples.get(0);
School school = firstTuplpe.get(0, School.class);
Student student = firstTuplpe.get(0, Student.class);
7. 結論
本文探討了使用子查詢、交叉連接和元組選擇等方法來建立 JPA Criteria 查詢,該查詢無需明確定義 JPA 關係即可連接資料庫表。這些方法為處理未定義實體關係的遺留模式提供了替代方案。
像往常一樣,我們的完整程式碼範例都可以在 GitHub 上找到。