在 Hibernate 中實現聯合
1. 簡介
在本教程中,我們將探討如何使用 Hibernate 中的union
操作來合併兩個相關資料庫實體的結果。在巴登大學,兼職講師和訪問研究員的資訊儲存在不同的表中;然而,在內部報告或搜尋等情況下,將他們作為一個群組進行查詢會很有幫助。
我們將學習如何使用 Hibernate 6 對union
的本機支援來執行此操作,以及在無法獲得直接支援或不切實際時手動實現類似 union 行為的替代方法。
2. 場景和設定
我們將使用兩個代表不同類型人員的簡單實體。這兩個實體具有相似的結構,僅在一個屬性上有所不同。這些共同屬性在使用union
語句時至關重要。
2.1. 創建Researcher
實體
我們將從Researcher
實體開始:
@Entity
public class Researcher {
@Id
private Long id;
private String name;
private boolean active;
// default getters and setters
}
我們還需要一個儲存庫介面:
@Repository
public interface ResearcherRepository extends JpaRepository<Researcher, Long> {}
2.2. 建立Lecturer
實體
然後,讓我們定義Lecturer
類,該類別的不同之處在於它有一個facultyId
欄位:
@Entity
public class Lecturer {
@Id
private Long id;
private String name;
private Integer facultyId;
// default getters and setters
}
現在,讓我們看看它的儲存庫:
@Repository
public interface LecturerRepository extends JpaRepository<Lecturer, Long> {}
2.3. 創建DTO
我們將使用 DTO 來表示統一的人員。此外,我們還將新增一個可用作鑑別列的role
屬性:
public class PersonDto {
private Long id;
private String name;
private String role;
// default getters, setters and constructors
}
2.4. 建立服務類
我們將把我們的方法集中在服務類別中,以使測試更簡單:
@Service
public class UnionService {
@PersistenceContext
EntityManager em;
@Autowired
LecturerRepository lecturerRepository;
@Autowired
ResearcherRepository researcherRepository;
@Autowired
UnionService unionService;
// ...
}
2.5. 建立測試數據
最後,我們將包含六行測試數據,其中一行重複,以區分union
和union all
。讓我們從測試類別開始:
@SpringBootTest
class UnionServiceIntegrationTest {
@Autowired
LecturerRepository lecturerRepository;
@Autowired
ResearcherRepository researcherRepository;
@BeforeEach
void setUp() {
// ...
}
}
現在,讓我們建立每次測試要用到的資料。我們將在講師和研究員的儲存庫中包含一個具有相同 ID 和姓名的人員:
lecturerRepository.saveAll(
List.of(
new Lecturer(1l, "Alice"), new Lecturer(2l, "Bob"), new Lecturer(3l, "Candace")
));
researcherRepository.saveAll(
List.of(
new Researcher(3l, "Candace"), new Researcher(4l, "Diana"), new Researcher(5l, "Elena")
));
3. 在 JPQL 查詢中使用 Union
從 Hibernate 6 開始,我們可以將union
語句直接與createQuery()
一起使用。給定一個合適的建構函數,我們可以在查詢中使用建構函數表達式;這會自動將PersonDto
對應到結果中。
我們來為UnionService
新增一個方法看看是什麼樣子的:
public List<PersonDto> fetch() {
return em.createQuery("""
select new PersonDto(l.id, l.name) from Lecturer l
union
select new PersonDto(r.id, r.name) from Researcher r
""", PersonDto.class)
.getResultList();
}
現在我們可以測試一下:
@Test
void whenUnionQuery_thenUnifiedResult() {
List<PersonDto> result = unionService.fetch();
assertEquals(5, result.size());
}
由於union
語句刪除了重複的行,我們得到了 5 個結果。而使用union all
語句則得到了 6 個結果。
4. 在記憶體中模擬 Union 語句
我們可以手動合併兩個查詢結果,模擬union
語句的行為。此外,我們還將觀察收集器類型如何影響結果。
4.1 合併到List
中
我們首先使用從擴展JpaRepository
中獲得的findAll()
方法收集兩個查詢的結果:
public List<PersonDto> fetchManually() {
List<Lecturer> lecturers = lecturerRepository.findAll();
List<Researcher> researchers = researcherRepository.findAll();
// ...
}
然後我們使用Stream.concat()
合併它們,並為我們的role
屬性選擇一個合適的值。由於我們將兩個流合併到一個List
中,因此我們將得到與union all
語句相同的結果:
return Stream.concat(
lecturers.stream().map(l -> new PersonDto(l.getId(), l.getName(), "LECTURER")),
researchers.stream().map(r -> new PersonDto(r.getId(), r.getName(), "RESEARCHER")))
.toList();
缺點顯而易見:由於需要進行兩次查詢而不是一次,效能會降低。此外,分頁也比較困難。所以它只適合合併小型表。
4.2 合併成一個Set
為了刪除重複項,我們可以將結果收集到Set
中:
return Stream.concat(
lecturers.stream().map(l -> new PersonDto(l.getId(), l.getName(), "LECTURER")),
researchers.stream().map(r -> new PersonDto(r.getId(), r.getName(), "RESEARCHER")))
.collect(Collectors.toSet());
要刪除重複的行,我們必須重寫PersonDto
中的equals()
和hashCode()
。我們只檢查id
和name
欄位(它們是實體之間的公共欄位),並省略 discriminator 欄位:
public int hashCode() {
return Objects.hash(id, name);
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
PersonDto other = (PersonDto) obj;
return Objects.equals(id, other.id) && Objects.equals(name, other.name);
}
5. 使用 Union 映射視圖
如果我們的union
查詢太複雜,我們可以在資料庫中建立它,然後僅在 Java 中映射它。
5.1. 建立視圖
為了演示,我們將建立一個簡單的視圖:
CREATE VIEW IF NOT EXISTS person_view AS
SELECT id, name, 'LECTURER' AS role FROM Lecturer
UNION
SELECT id, name, 'RESEARCHER' AS role FROM Researcher
5.2. 使用原生查詢呼叫視圖
然後,我們將在UnionService
中建立一個新方法,使用createNativeQuery()
將union
結果直接對應到PersonDto
:
public List<PersonDto> fetchView() {
return em.createNativeQuery(
"select e.id, e.name, e.role from person_view e",
PersonDto.class)
.getResultList();
}
5.3. 使用儲存庫查詢呼叫視圖
我們也可以使用@Query
註解來建立一個投影介面。**如果在這裡重複使用PersonDto
,則會拋出ConverterNotFoundException.
**因此,讓我們建立一個新的介面:
public interface PersonView {
Long getId();
String getName();
String getRole();
}
然後,我們將在LecturerRepository
中新增一個查詢方法,將nativeQuery
設為true
並傳回我們建立的PersonView
類型:
@Query(value = "select e.id, e.name, e.role from person_view e", nativeQuery = true)
List<PersonView> findPersonView();
6. 使用CriteriaBuilder
Hibernate 的CriteriaBuilder
實作也獲得了對union
語句的支援。
6.1. 解開 Hibernate Session
由於union
語句不是標準 JPA API 的一部分,因此我們需要解開Session
來存取 Hibernate 的實作:
public List<PersonDto> fetchWithCriteria() {
var session = em.unwrap(Session.class);
var builder = session.getCriteriaBuilder();
// ...
}
6.2. 生成子查詢
我們將分別建立每個子查詢,從講師子查詢開始。我們將PersonDto
傳遞給builder.createQuery()
方法,然後由此建構一個Root<Lecturer>
:
CriteriaQuery<PersonDto> lecturerQuery = builder.createQuery(PersonDto.class);
Root<Lecturer> lecturer = lecturerQuery.from(Lecturer.class);
最後,我們使用construct()
方法建構一個類似PersonDto-
的 SELECT 子句,就像之前一樣。我們可以使用literal()
方法包含根類型中沒有的欄位:
lecturerQuery.select(builder.construct(
PersonDto.class, lecturer.get("id"), lecturer.get("name"), builder.literal("LECTURER")));
然後,我們對研究人員也做同樣的事情:
CriteriaQuery<PersonDto> researcherQuery = builder.createQuery(PersonDto.class);
Root<Researcher> researcher = researcherQuery.from(Researcher.class);
researcherQuery.select(builder.construct(
PersonDto.class, researcher.get("id"), researcher.get("name"), builder.literal("RESEARCHER")));
6.3 合併結果
最終,我們將呼叫unionAll()
並傳回結果:
var unionQuery = builder.unionAll(lecturerQuery, researcherQuery);
return session.createQuery(unionQuery).getResultList();
這種方法比較冗長,但在建立動態查詢時特別有用,因為我們對每一步都有絕對的控制權。
7. 結論
在本文中,我們探討了在 Hibernate 中實現union
查詢的幾種方法,並以擴展柏林伯爾登大學 (Baeldung University) 的模式為例。我們了解了 Hibernate 6+ 如何在 JPQL 和 Criteria API 中提供對union
的原生支持,從而高效地合併來自不同實體的結果。
對於原生支援不可用或不切實際的情況,我們討論了其他替代方案,例如在記憶體中合併結果或映射資料庫視圖。每種方法都會在效能、可維護性和分頁支援方面有所取捨。
與往常一樣,原始碼可在 GitHub 上取得。