Hibernate 中的 TupleTransformer 和 ResultListTransformer
1. 簡介
在本教學中,我們將探索如何使用TupleTransformer
進行行級轉換,以及如何ResultListTransformer
進行清單級處理。雖然 JPQL 投影可以解決許多情況,但有時我們需要更大的彈性,例如繪製複雜的 DTO、重構查詢結果或對整個結果集進行後處理。
2. 場景和設定
在 Hibernate 6 之前,自訂結果映射由ResultTransformer
介面處理。此介面功能強大,但功能過載,因為它同時處理每行和每個清單的轉換。
在 6 中,這些職責被拆分為兩個介面:
-
TupleTransformer
:轉換每個結果行 -
ResultListTransformer
:對整個結果清單進行後處理
基於 Baeldung University 模式,現在讓我們使用這些新的 Hibernate 介面將我們的實體映射並轉換為 DTO。
2.1. 建立實體
我們將從這樣一個場景開始:每個學生都有一個他們就讀的本系:
@Entity
public class Department {
@Id
@GeneratedValue
Long id;
String name;
@OneToMany(mappedBy = "department")
List<Student> students;
// standard getters, setters, and constructors
}
例如,一名學生可能在電腦科學系攻讀電腦科學學士學位:
@Entity
public class Student {
@Id
@GeneratedValue
Long id;
String name;
@ManyToOne
Department department;
// standard getters, setters, and constructors
}
最後,在轉換中引用一個簡單的 DTO:
public record StudentDto(Long id, String name) {}
2.2. 載入測試數據
我們的數據涉及將一些Student
註冊到Course
並將他們分配到Departments
:
@Transactional
@SpringBootTest
class HibernateTransformersIntegrationTest {
@PersistenceContext
EntityManager em;
@BeforeEach
void setUp() {
Department cs = new Department("Computer Science");
Department math = new Department("Mathematics");
em.persist(cs);
em.persist(math);
Student alice = new Student("Alice", cs);
Student bob = new Student("Bob", cs);
Student carol = new Student("Carol", math);
em.persist(alice);
em.persist(bob);
em.persist(carol);
}
// ...
}
這足以讓我們了解TupleTransformer
是如何運作的。
3. 使用TupleTransformer
TupleTransformer
定義了我們希望應用於每個查詢結果的轉換。此外,該轉換在結果打包到List
並返回之前套用。最重要的是,每個結果都以Object[]
的形式接收,我們可以將其轉換為任何其他類型。
3.1 映射簡單查詢的結果
要從使用EntityManager
建立的查詢中實作TupleTransformer
,我們首先需要呼叫unwrap()
來存取 Hibernate 的 API。然後,我們使用setTupleTransformer()
來存取每一行及其列的別名:
@Test
void whenUsingTupleTransformer_thenMapToStudentDto() {
List<StudentDto> results = em.createQuery("SELECT s.id, s.name FROM Student s")
.unwrap(Query.class)
.setTupleTransformer(
(tuple, aliases) -> new StudentDto((Long) tuple[0], (String) tuple[1]))
.getResultList();
assertEquals(3, results.size());
}
透過索引存取列,將每一行對應到StudentDto
。
3.2. 使用列別名按名稱映射
如果我們為查詢定義別名,我們可以使用aliases
數組更安全地映射 DTO:
String query = "SELECT s.id AS studentId, s.name AS studentName FROM Student s";
然後,我們像往常一樣建立查詢:
em.createQuery(query).unwrap(Query.class)
.setTupleTransformer((tuple, aliases) -> {
// ...
})
.getResultList();
TupleTransformer
為我們提供了完全的自由來對應行。首先,我們建立一個 map,將每個列的值對應到一個帶有別名的鍵上:
Map<String, Object> row = IntStream.range(0, aliases.length).boxed()
.collect(Collectors.toMap(i -> aliases[i], i -> tuple[i]));
最後,我們可以透過名稱存取 DTO 的屬性:
return new StudentDto((Long) row.get("studentId"), (String) row.get("studentName"));
這避免了基於索引的存取的錯誤,對於具有許多欄位的 DTO 特別有用。
4. 使用ResultListTransformer
將結果分組
ResultListTransformer
充當結果清單的後處理器,賦予我們超越 SQL 表達能力的靈活性。讓我們建立一個 DTO,其中包含一個Department
以及每行一個Students
清單:
public record DepartmentStudentsDto(String department, List<String> students) {}
現在,讓我們用它將行分組到這個更高層級的結構中:
em.createQuery("SELECT d.name, s.name FROM Department d JOIN d.students s")
.unwrap(Query.class)
.setTupleTransformer(
(tuple, aliases) -> new AbstractMap.SimpleEntry<>((String) tuple[0], (String) tuple[1]))
.setResultListTransformer(list -> ((List<Map.Entry<String, String>>) list).stream()
.collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, toList())))
.entrySet()
.stream().map(e -> new DepartmentStudentsDto(e.getKey(), e.getValue()))
.toList())
.getResultList();
分解一下:我們從(department name, student name)
條目開始,按系所分組,並將學生資料收集到清單中。結果是一個Map<String, List<String>>
,然後我們將其對應到 DTO 中:
{
"Computer Science" -> ["Alice", "Bob"],
"Mathematics" -> ["Carol"]
}
大多數情況下,應該使用 JPQL 聚合函數對資料庫進行分組,因為資料庫已針對集合操作進行了最佳化。但是,當分組邏輯與 SQL 無法完美地對應時(例如建立此巢狀 DTO 時,或在行級處理後套用轉換時), setResultListTransformer()
會很有用。
5. 使用ResultListTransformer
刪除重複項
為了示範重複資料刪除,我們引入了兩個實體: Course
和Enrollment
。一門課程可以有多位學生註冊,而一位學生也可以註冊多門課程。
5.1. 創建關係
我們將從簡單的開始,僅包含Course's
名稱和 ID:
@Entity
public class Course {
@Id
@GeneratedValue
Long id;
String name;
// standard getters, setters, and constructors
}
然後,為Student
和Course
添加帶有@ManyToOne
註釋的Enrollment
實體:
@Entity
public class Enrollment {
@Id
@GeneratedValue
Long id;
@ManyToOne
Student student;
@ManyToOne
Course course;
// standard getters, setters, and constructors
}
5.2. 插入新的測試數據
讓我們回到設定方法並插入Enrollment
關係:
@BeforeEach
void setUp() {
//...
Course algorithms = new Course("Algorithms", cs);
Course calculus = new Course("Calculus", math);
em.persist(algorithms);
em.persist(calculus);
em.persist(new Enrollment(alice, algorithms));
em.persist(new Enrollment(alice, calculus));
em.persist(new Enrollment(bob, algorithms));
em.persist(new Enrollment(carol, calculus));
}
5.3. 建立ResultListTransformer
如果我們查詢註冊信息並加入學生信息,同一個學生在他們所報的每門課程中都會出現一次,從而導致重複。因此, Enrollment
是一個很自然的例子,可以用來展示如何使用ResultListTransformer
來消除重複:
String query = "SELECT s.id, s.name FROM Enrollment e JOIN e.student s";
當我們呼叫setResultListTransformer()
時,我們可以存取結果流。讓我們進行一些後處理,並對結果呼叫distinct()
:
List results = em.createQuery(query)
.unwrap(Query.class)
.setTupleTransformer((tuple, aliases) -> new StudentDto((Long) tuple[0], (String) tuple[1]))
.setResultListTransformer(list -> list.stream()
.distinct()
.toList())
.getResultList();
distinct()
在這裡起作用是因為 Java 記錄透過比較每個欄位隱含地實作了equals()
:
assertEquals(3, results.size());
在 SQL 查詢中直接使用 distinct 效率更高,但是當必須在 DTO 轉換後進行重複資料刪除時, ResultListTransformer
很有用。
6. 結論
在本文中,我們探討了 Hibernate 如何區分按行轉換和按列表轉換。我們也了解了TupleTransformer
如何安全地將查詢結果對應到 DTO,甚至利用別名來增強健全性,以及ResultListTransformer
如何刪除重複項或將結果分組到更高層級的結構中。這些工具使我們能夠靈活地根據應用程式需求自訂查詢輸出,而不會使實體模型或查詢過於複雜。
與往常一樣,原始碼可在 GitHub 上取得。