Рубрики
Без рубрики

Как получить один ко многим ДЛЯ проекции с помощью JPA и перехода в спящий режим

Узнайте, как получить отношение таблицы “один ко многим” в качестве проекции DTO при использовании JPA и преобразователя результатов Hibernate.

Автор оригинала: Vlad Mihalcea.

Вступление

В этой статье я собираюсь показать вам, как вы можете получить соотношение “один ко многим” в качестве проекции DTO при использовании JPA и Hibernate.

В то время как сущности позволяют очень легко извлекать дополнительные отношения, когда дело доходит до прогнозов, для достижения этой цели вам необходимо использовать ResultTransformer .

Связи таблиц

Давайте предположим, что у нас есть следующее сообщение и post_comment таблицы, которые образуют отношение один ко многим через столбец post_id Внешний ключ в таблице post_comment|/.

Получение проекции сущности JPA “один ко многим”

Вышеупомянутая запись таблица может быть сопоставлена со следующей Должность организация:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    @Column(name = "created_on")
    private LocalDateTime createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @Column(name = "updated_on")
    private LocalDateTime updatedOn;

    @Column(name = "updated_by")
    private String updatedBy;

    @Version
    private Integer version;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List comments = new ArrayList<>();
    
    //Getters and setters omitted for brevity
    
    public Post addComment(PostComment comment) {
        comments.add(comment);
        comment.setPost(this);
        return this;
    }
}

И таблица post_comment сопоставляется со следующей сущностью Комментарий к сообщению :

@Entity
@Table(name = "post_comment")
public class PostComment {

    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

Отношение таблицы “один ко многим” отображается как двунаправленная ассоциация @OneToMany JPA , и по этой причине мы можем легко извлечь ассоциацию, используя директиву JOIN FETCH JPQL:

List posts = entityManager.createQuery("""
    select distinct p
    from Post p
    join fetch p.comments pc
    order by pc.id
    """)
.setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
.getResultList();

HINT_PASS_DISTINCT_THROUGH необходим, так как мы не хотим, чтобы ОТДЕЛЬНОЕ ключевое слово JPQL передавалось в базовый SQL-запрос. Для получения более подробной информации об этой подсказке запроса JPA ознакомьтесь с этой статьей .

При выполнении приведенного выше запроса JPQL Hibernate генерирует следующую инструкцию SQL:

SELECT p.id AS id1_0_0_,
       pc.id AS id1_1_1_,
       p.created_by AS created_2_0_0_,
       p.created_on AS created_3_0_0_,
       p.title AS title4_0_0_,
       p.updated_by AS updated_5_0_0_,
       p.updated_on AS updated_6_0_0_,
       p.version AS version7_0_0_,
       pc.post_id AS post_id3_1_1_,
       pc.review AS review2_1_1_,
       pc.post_id AS post_id3_1_0__,
       pc.id AS id1_1_0__
FROM post p
INNER JOIN post_comment pc ON p.id=pc.post_id
ORDER BY pc.id

Проекция сущности выбирает все свойства сущности, и, хотя это очень полезно, когда мы хотим изменить сущность, для проекций только для чтения это может быть накладными расходами.

Получение проекции “один ко многим” с помощью JPA и гибернации

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

Однако мы не хотим использовать табличный Набор результатов или проекцию запроса по умолчанию List JPA или Hibernate. Мы хотим преобразовать вышеупомянутый набор результатов запроса в Список объектов PostDTO , каждый такой объект имеет комментарии коллекцию, содержащую все связанные PostCommentDTO объекты:

Как я объяснил в этой статье , мы можем использовать спящий режим Преобразователь результатов , как показано в следующем примере:

List 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 определит отображение между Объектом[] проекцией и PostDTO объектом, содержащим PostCommentDTO дочерние объекты DTO:

public class PostDTOResultTransformer 
        implements ResultTransformer {

    private Map postDTOMap = new LinkedHashMap<>();

    @Override
    public Object transformTuple(
            Object[] tuple, 
            String[] aliases) {
            
        Map 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());
    }
}

Псевдоним для индексирования карты – это всего лишь небольшая утилита, которая позволяет нам создать структуру Map , которая связывает псевдонимы столбцов и индекс, в котором значение столбца находится в Объекте[] кортеж массив:

public  Map aliasToIndexMap(
        String[] aliases) {
    
    Map aliasToIndexMap = new LinkedHashMap<>();
    
    for (int i = 0; i < aliases.length; i++) {
        aliasToIndexMap.put(aliases[i], i);
    }
    
    return aliasToIndexMap;
}

postdatamap – это место, где мы собираемся хранить все PostDTO сущности, которые в конечном итоге будут возвращены при выполнении запроса. Причина, по которой мы используем postdatamap , заключается в том, что родительские строки дублируются в SQL-запросе набор результатов для каждой дочерней записи.

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

Класс PostDTO имеет конструктор, который может задавать идентификатор и заголовок свойства с использованием псевдонимов выделенных столбцов:

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 comments = new ArrayList<>();

    public PostDTO(
            Object[] tuples, 
            Map aliasToIndexMap) {
            
        this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]);
        this.title = stringValue(tuples[aliasToIndexMap.get(TITLE_ALIAS)]);
    }

    //Getters and setters omitted for brevity
}

Комментарий к сообщению D К построен аналогичным образом:

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

Вывод

В то время как сущности очень упрощают извлечение связей, выбор всех столбцов неэффективен, если нам нужно только подмножество свойств сущности.

С другой стороны, проекции TO более эффективны с точки зрения извлечения SQL, но требуют небольшой работы для сопоставления родительских и дочерних DTO. К счастью, Hibernate Resultтрансформатор предлагает очень гибкое решение этой проблемы, и мы можем получить отношение “один ко многим” даже в качестве проекции DTO.