简体   繁体   中英

How to map @OneToMany field into List<DTO> when using JPQL or HQL?

I have the following entities:

@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...
}

I have the following HQL query, in which I select a subset of 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 = '' ) "

However, since there are too many fields in CityExpert , I don't want to select all fields. Hence, I have changed the query as follows:

"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 = '' ) "

However, apparently we cannot select a one-to-many field in an entity like that, because I am getting a MySQLSyntaxErrorException with the preceding query (refer to this question ). Hence, I have changed the query to following:

"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 = '' ) "

However, this time the result becomes a List<Object[]> , instead of List<CityExpert> .

I have created the following DTO:

public class CityExpertDTO {

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

}

However, I don't know how I should map the result returned by Hibernate to List<CityExpertDTO> . I mean, I can do this manually but surely there must be an automated solution provided by Hibernate.

I am using Spring Data JPA and using the HQL as follows:

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);

}

How can I map the result to CityExpertDTO ?

Table relationships

Assuming we have the following post and post_comment tables, which form a one-to-many relationship via the post_id Foreign Key column in the post_comment table.

用于 JPA DTO 投影的 post 和 post_comment 表

SQL Projection

Considering we have a use case that only requires fetching the id and title columns from the post table, as well as the id and review columns from the post_comment tables, we could use the following JPQL query to fetch the required projection:

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

When running the projection query above, we get the following results:

| 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 Projection

However, we don't want to use a tabular-based ResultSet or the default List<Object[]> JPA or Hibernate query projection. We want to transform the aforementioned query result set to a List of PostDTO objects, each such object having a comments collection containing all the associated PostCommentDTO objects:

用于 DTO 投影的 PostDTO 和 PostCommentDTO

We can use a Hibernate ResultTransformer , as illustrated by the following example:

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());

The PostDTOResultTransformer is going to define the mapping between the Object[] projection and the PostDTO object containing the PostCommentDTO child DTO objects:

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());
    }
}

The aliasToIndexMap is just a small utility that allows us to build a Map structure that associates the column aliases and the index where the column value is located in the Object[] tuple array:

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;
}

The postDTOMap is where we are going to store all PostDTO entities that, in the end, will be returned by the query execution. The reason we are using the postDTOMap is that the parent rows are duplicated in the SQL query result set for each child record.

The computeIfAbsent method allows us to create a PostDTO object only if there is no existing PostDTO reference already stored in the postDTOMap .

The PostDTO class has a constructor that can set the id and title properties using the dedicated column aliases:

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
}

The PostCommentDTO is built in a similar fashion:

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
}

That's it!

Using the PostDTOResultTransformer , the SQL result set can be transformed into a hierarchical DTO projection, which is much convenient to work with, especially if it needs to be marshalled as a JSON response:

postDTOs = {ArrayList}, size = 2
  0 = {PostDTO} 
    id = 1L
    title = "High-Performance Java Persistence"
    comments = {ArrayList}, size = 2
      0 = {PostCommentDTO} 
        id = 1L
        review = "Best book on JPA and Hibernate!"
      1 = {PostCommentDTO} 
        id = 2L
        review = "A must read for every Java developer!"
  1 = {PostDTO} 
    id = 2L
    title = "Hypersistence Optimizer"
    comments = {ArrayList}, size = 1
      0 = {PostCommentDTO} 
       id = 3L
       review = "It's like pair programming with Vlad!"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM