Как сопоставить поле @OneToMany со списком‹DTO› при использовании JPQL или 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, в котором я выбираю подмножество CityExperts:

"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 с предыдущим запросом (см. этом вопросе). Следовательно, я изменил запрос на следующий:

"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?


person Utku    schedule 25.12.2017    source источник
comment
Я думаю, вы не можете напрямую сопоставить запись в dto. У меня была такая же проблема, и я просто создал собственный запрос, выбрал все данные и передал их через цикл for!   -  person Mithat Konuk    schedule 25.12.2017


Ответы (1)


Отношения между таблицами

Предположим, что у нас есть следующие таблицы post и post_comment, которые формируют отношение "один ко многим" через столбец post_id внешнего ключа в таблице post_comment.

«Таблицы

SQL-проекция

Учитывая, что у нас есть вариант использования, который требует только выборки столбцов id и title из таблицы post, а также столбцов id и review из таблиц post_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 объекты:

PostDTO и PostCommentDTO, используемые для проекции DTO

Мы можем использовать Hibernate 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, содержащим PostCommentDTO дочерних объектов DTO:

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[] 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 сущности, которые, в конце концов, будут возвращены при выполнении запроса. Причина, по которой мы используем postDTOMap, заключается в том, что родительские строки дублируются в наборе результатов SQL-запроса для каждой дочерней записи.

Метод computeIfAbsent позволяет нам создать объект PostDTO только в том случае, если в файле postDTOMap уже нет существующей ссылки PostDTO.

Класс PostDTO имеет конструктор, который может устанавливать свойства id и 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
}

Вот и все!

Используя PostDTOResultTransformer, набор результатов SQL можно преобразовать в иерархическую проекцию DTO, с которой очень удобно работать, особенно если ее нужно маршалировать как ответ JSON:

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!"
person Vlad Mihalcea    schedule 14.05.2020