Автор оригинала: 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 Listcomments = 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:
Listposts = 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
объекты:
Как я объяснил в этой статье , мы можем использовать спящий режим Преобразователь результатов
, как показано в следующем примере:
ListpostDTOs = 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 MappostDTOMap = 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 MapaliasToIndexMap( 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 Listcomments = 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, MapaliasToIndexMap) { 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.