1. 引言
本文将探讨如何在Hibernate中使用union操作整合两个相关数据库实体的查询结果。在Baeldung大学系统中,兼职讲师和访问研究员的信息存储在独立表中,但在内部报表或搜索场景下,需要将它们作为统一组查询。
我们将学习使用Hibernate 6原生支持的union功能,以及当直接支持不可行时手动实现类union行为的替代方案。
2. 场景与设置
使用两个简单的实体表示不同类型的人员。两个实体结构相似,仅有一个属性不同——这些共同属性是使用union语句的关键。
2.1 创建Researcher实体
先定义Researcher实体:
@Entity
public class Researcher {
@Id
private Long id;
private String name;
private boolean active;
// 默认getter和setter
}
配套的Repository接口:
@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;
// 默认getter和setter
}
其Repository接口:
@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;
// 默认getter、setter和构造方法
}
2.4 创建Service类
将各种实现方式集中到Service类中,便于测试:
@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开始,可直接通过createQuery()使用union语句。配合适当的构造函数,可在查询内使用构造表达式自动映射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字段,排除标识列:
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()**直接映射结果到PersonDto:
public List<PersonDto> fetchView() {
return em.createNativeQuery(
"select e.id, e.name, e.role from person_view e",
PersonDto.class)
.getResultList();
}
5.3 通过Repository查询调用视图
也可用**@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 生成子查询
分别创建子查询,先处理讲师查询。向**builder.createQuery()传入PersonDto,并构建Root
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. 总结
本文以Baeldung大学系统为例,探讨了Hibernate中实现union查询的多种方式。Hibernate 6+在JPQL和Criteria API中均原生支持union,可高效整合不同实体的结果。
当原生支持不可行时,替代方案包括内存合并结果或映射数据库视图。各方案在性能、可维护性和分页支持上存在权衡。
完整源码可在GitHub获取。