如何从HQL结果将@OneToMany字段映射到List <dto>?

时间:2017-12-25 01:23:07

标签: java spring hibernate jpa hql

我有以下实体:

@Entity
public class CityExpert {
    @Id
    private long id;

    @OneToOne
    private User user;

    @OneToMany(mappedBy = "cityExpert")
    private List<CityExpertDocument> documents;

    // Lots of other fields...
}

@Entity
public class CityExpertDocument {

    @Id
    private long id;

    @ManyToOne
    private CityExpert cityExpert;

    // Lots of other fields...
}

@Entity
public class User {
    @Id
    private long id;

    private String name;

    private String email;

    // Lots of other fields...
}

我有以下HQL查询,其中我选择了CityExpert s的一个子集:

"select " +
        "e " +
"from " +
        "CityExpert e " +
"where " +
        "( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
        "and " +
        "( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "

但是,由于CityExpert中的字段太多,我不想选择所有字段。因此,我更改了查询如下:

"select " +
        "e.user.name, " +
        "e.user.email, " +
        "e.documents " +
"from " +
        "CityExpert e " +
"where " +
        "( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
        "and " +
        "( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "

然而,显然我们无法在这样的实体中选择一对多字段,因为我得到了前面查询的MySQLSyntaxErrorException(请参阅this question)。因此,我已将查询更改为以下内容:

"select " +
        "e.user.name, " +
        "e.user.email, " +
        "d " +
"from " +
        "CityExpert e " +
        "left join " +
        "e.documents d" +
"where " +
        "( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
        "and " +
        "( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "

但是,这次结果变为List<Object[]>,而不是List<CityExpert>

我创建了以下DTO:

public class CityExpertDTO {

    private String name;
    private String email;
    private List<CityExpertDocument> documents;

}

但是,我不知道如何将Hibernate返回的结果映射到List<CityExpertDTO>。我的意思是,我可以手动完成,但肯定必须有Hibernate提供的自动化解决方案。

我正在使用Spring Data JPA并使用HQL,如下所示:

public interface CityExpertRepository extends JpaRepository<CityExpert, Long> {

    @Query(
            "select " +
                    "e " +
            "from " +
                    "CityExpert e " +
            "where " +
                    "( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
                    "and " +
                    "( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "
    )
    Set<CityExpert> findUsingNameAndPhoneNumber(String name,
                                                String phoneNumber);

}

如何将结果映射到CityExpertDTO

1 个答案:

答案 0 :(得分:1)

这是一个非常常见的问题,因此此答案基于我在博客上写的this post

表关系

假设我们有以下postpost_comment表,它们通过post_id表中的post_comment外键列构成one-to-many relationship

The post and post_comment tables used for the JPA DTO projection

SQL投影

考虑到我们有一个用例,只需要从id表以及titlepost中提取idreviewpost_comment表中的列中,我们可以使用以下JPQL查询来获取所需的投影:

select p.id as p_id, 
       p.title as p_title,
       pc.id as pc_id, 
       pc.review as pc_review
from PostComment pc
join pc.post p
order by pc.id

运行上面的投影查询时,我们得到以下结果:

| p.id | p.title                           | pc.id | pc.review                             |
|------|-----------------------------------|-------|---------------------------------------|
| 1    | High-Performance Java Persistence | 1     | Best book on JPA and Hibernate!       |
| 1    | High-Performance Java Persistence | 2     | A must-read for every Java developer! |
| 2    | Hypersistence Optimizer           | 3     | It's like pair programming with Vlad! |

DTO投影

但是,我们不想使用基于表格的ResultSet或默认的List<Object[]> JPA或Hibernate查询投影。我们想将上述查询结果集转换为List个对象的PostDTO,每个这样的对象都有一个comments集合,其中包含所有关联的PostCommentDTO对象:

The PostDTO and PostCommentDTO used for DTO projection

正如我在this article中所述,我们可以使用休眠ResultTransformer,如以下示例所示:

List<PostDTO> postDTOs = entityManager.createQuery("""
    select p.id as p_id, 
           p.title as p_title,
           pc.id as pc_id, 
           pc.review as pc_review
    from PostComment pc
    join pc.post p
    order by pc.id
    """)
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new PostDTOResultTransformer())
.getResultList();

assertEquals(2, postDTOs.size());
assertEquals(2, postDTOs.get(0).getComments().size());
assertEquals(1, postDTOs.get(1).getComments().size());

PostDTOResultTransformer将定义Object[]投影与包含PostDTO子DTO对象的PostCommentDTO对象之间的映射:

public class PostDTOResultTransformer 
        implements ResultTransformer {

    private Map<Long, PostDTO> postDTOMap = new LinkedHashMap<>();

    @Override
    public Object transformTuple(
            Object[] tuple, 
            String[] aliases) {

        Map<String, Integer> aliasToIndexMap = aliasToIndexMap(aliases);

        Long postId = longValue(tuple[aliasToIndexMap.get(PostDTO.ID_ALIAS)]);

        PostDTO postDTO = postDTOMap.computeIfAbsent(
            postId, 
            id -> new PostDTO(tuple, aliasToIndexMap)
        );

        postDTO.getComments().add(
            new PostCommentDTO(tuple, aliasToIndexMap)
        );

        return postDTO;
    }

    @Override
    public List transformList(List collection) {
        return new ArrayList<>(postDTOMap.values());
    }
}

aliasToIndexMap只是一个很小的实用程序,它使我们可以构建一个Map结构,该结构将列别名与列值位于Object[] {{1中的索引}}数组:

tuple

public Map<String, Integer> aliasToIndexMap( String[] aliases) { Map<String, Integer> aliasToIndexMap = new LinkedHashMap<>(); for (int i = 0; i < aliases.length; i++) { aliasToIndexMap.put(aliases[i], i); } return aliasToIndexMap; } 是我们将存储所有postDTOMap实体的位置,最后,这些实体将由查询执行返回。我们使用PostDTO的原因是,每个子记录的SQL查询结果集中都有父行。

postDTOMap方法仅在computeIfAbsent中没有存储现有的PostDTO引用的情况下,才可以创建PostDTO对象。

postDTOMap类具有一个构造函数,可以使用专用的列别名来设置PostDTOid属性:

title

public class PostDTO { public static final String ID_ALIAS = "p_id"; public static final String TITLE_ALIAS = "p_title"; private Long id; private String title; private List<PostCommentDTO> comments = new ArrayList<>(); public PostDTO( Object[] tuples, Map<String, Integer> aliasToIndexMap) { this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]); this.title = stringValue(tuples[aliasToIndexMap.get(TITLE_ALIAS)]); } //Getters and setters omitted for brevity } 的构建方式类似:

PostCommentDTO

就是这样!

使用public class PostCommentDTO { public static final String ID_ALIAS = "pc_id"; public static final String REVIEW_ALIAS = "pc_review"; private Long id; private String review; public PostCommentDTO( Object[] tuples, Map<String, Integer> aliasToIndexMap) { this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]); this.review = stringValue(tuples[aliasToIndexMap.get(REVIEW_ALIAS)]); } //Getters and setters omitted for brevity } ,可以将SQL结果集转换为分层DTO投影,使用起来非常方便,尤其是当它需要作为JSON响应编组时:

PostDTOResultTransformer