JPA 查询允许您获取实体或 DTO 投影。但是,有时需要一个组合的结果集。

实体模型

假设我们有下面两个实体:

Person Country 这两个实体没有通过 @ManyToOne 进行关联。但是,两个实体共享一个 locale 属性,我们可以使用它来在两者之间形成连接。

在 DTO 投影中返回实体

考虑我们将做下面的 DTO 的映射:

public class PersonAndCountryDTO{
 
    private final Person person;
 
    private final String country;
 
    public PersonAndCountryDTO(
        Person person, 
        String country) {
        this.person = person;
        this.country = country;
    }
 
    public Person getPerson() {
        return person;
    }
 
    public String getCountry() {
        return country;
    }
}

使用 JPQL 查询大概长这样:

List<PersonAndCountryDTO> personAndAddressDTOs = entityManager.createQuery(
    "select new " +
    "   com.vladmihalcea.book.hpjp.hibernate.query.dto.PersonAndCountryDTO(" +
    "       p, " +
    "       c.name" +
    "   ) " +
    "from Person p " +
    "join Country c on p.locale = c.locale " +
    "order by p.id", PersonAndCountryDTO.class)
.getResultList();

hibernate 生成的 sql 如下:

SELECT p.id AS col_0_0_,
       c.name AS col_1_0_
FROM   Person p
INNER JOIN
       Country c 
ON
       ( p.locale = c.locale )
ORDER BY
       p.id
 
SELECT p.id AS id1_1_0_,
       p.locale AS locale2_1_0_,
       p.name AS name3_1_0_
FROM   Person p
WHERE  p.id = 3
 
SELECT p.id AS id1_1_0_,
       p.locale AS locale2_1_0_,
       p.name AS name3_1_0_
FROM   Person p
WHERE  p.id = 4

Hibernate 5.2 实现的 DTO 投影无法在不执行辅助查询的情况下实现 ResultSet 中的 DTO 投影。但是,这对性能非常不利,因为它可能导致 N + 1 查询问题。

Hibernate 6.0 新的 SQM 解析器可能会解决这个问题,

ResultTransformer

但是,您不仅限于使用 JPA。 Hibernate 提供了许多在标准中没有直接定义的增强功能。其中一个增强功能是 ResultTransformer 机制,它允许您以任何方式自定义ResultSet

List<PersonAndCountryDTO> personAndAddressDTOs = entityManager
.createQuery(
    "select p, c.name " +
    "from Person p " +
    "join Country c on p.locale = c.locale " +
    "order by p.id")
.unwrap( org.hibernate.query.Query.class )
.setResultTransformer( 
    new ResultTransformer() {
        @Override
        public Object transformTuple(
            Object[] tuple, 
            String[] aliases) {
            return new PersonAndCountryDTO(
                (Person) tuple[0],
                (String) tuple[1]
            );
        }
 
        @Override
        public List transformList(List collection) {
            return collection;
        }
    } 
)
.getResultList();

此查询需要思考两件事:

  1. unwrap 方法用于将 JPA javax.persistence.Query 转换为特定于 Hibernate 的 org.hibernate.query.Query,以便我们可以访问 setResultTransformer 方法。
  2. ResultTransformer 附带一个未遵循函数接口语法的旧定义。因此,在这个例子中我们不能使用 lambda。Hibernate 6.0 旨在克服这个问题,因此不推荐使用Hibernate ORM 5.2 ResultTransformer。尽管如此,还是会提供一种替代方案。

运行上述 Hibernate ResultTransformer 查询时,Hibernate 会生成以下输出:

SELECT p.id AS col_0_0_,
       c.name AS col_1_0_,
       p.id AS id1_1_,
       p.locale AS locale2_1_,
       p.name AS name3_1_
FROM   Person p
INNER JOIN
       Country c 
ON
       ( p.locale = c.locale )
ORDER BY
       p.id