Автор оригинала: Vlad Mihalcea.
Вступление
В этой статье я собираюсь объяснить, как использовать сопоставление JPA SQLRESULTSET, а также параметры Entity Result, ConstructorResult и ColumnResult.
Модель предметной области
Давайте рассмотрим, что у нас есть следующие таблицы post
и post_comment
в нашей базе данных:
Мы собираемся создать 50 записей
строк, каждая запись
с 5 дочерними записями//post_comment//.
LocalDateTime timestamp = LocalDateTime.of( 2016, 10, 9, 12, 0, 0, 0 ); LongStream.rangeClosed(1, POST_COUNT) .forEach(postId -> { Post post = new Post() .setId(postId) .setTitle( String.format( "High-Performance Java Persistence - Chapter %d", postId ) ) .setCreatedOn( Timestamp.valueOf(timestamp.plusDays(postId)) ); LongStream.rangeClosed(1, COMMENT_COUNT) .forEach(commentOffset -> { long commentId = ((postId - 1) * COMMENT_COUNT) + commentOffset; post.addComment( new PostComment() .setId(commentId) .setReview( String.format("Comment nr. %d - A must read!", commentId) ) .setCreatedOn( Timestamp.valueOf( timestamp .plusDays(postId) .plusMinutes(commentId) ) ) ); }); entityManager.persist(post); });
Затем мы выполним несколько собственных SQL-запросов и посмотрим, как мы можем извлекать DTO, сущности или смешивать сущности со скалярными значениями.
Сопоставление JPA SqlResultSetMapping
Аннотация SqlResultSetMapping
JPA выглядит следующим образом:
@Repeatable(SqlResultSetMappings.class) @Target({TYPE}) @Retention(RUNTIME) public @interface SqlResultSetMapping { String name(); EntityResult[] entities() default {}; ConstructorResult[] classes() default {}; ColumnResult[] columns() default {}; }
Аннотация SqlResultSetMapping
повторяется и применяется на уровне класса сущностей. Помимо присвоения уникального имени, которое используется Hibernate для регистрации сопоставления, существует три варианта сопоставления:
Результат сущности
Результат конструктора
Результат столбца
Далее мы рассмотрим, как работают все эти три варианта отображения, а также варианты использования, в которых вам потребуется их использовать.
JPA SqlResultSetMapping – EntityResult
Параметр Результат сущности
позволяет сопоставить столбцы JDBC Набор результатов
с одной или несколькими сущностями JPA.
Давайте предположим, что мы хотим получить первые 5 Сообщений
сущностей вместе со всеми связанными с ними Комментариями к сообщению
сущностями, которые соответствуют заданному заголовку
шаблону.
Как я объяснил в этой статье , мы можем использовать DENSE_RANK
Функцию окна SQL , чтобы знать, как фильтровать post
и post_comment
присоединенные записи, как показано в следующем SQL-запросе:
SELECT * FROM ( SELECT *, DENSE_RANK() OVER ( ORDER BY "p.created_on", "p.id" ) rank FROM ( SELECT p.id AS "p.id", p.created_on AS "p.created_on", p.title AS "p.title", pc.post_id AS "pc.post_id", pc.id as "pc.id", pc.created_on AS "pc.created_on", pc.review AS "pc.review" FROM post p LEFT JOIN post_comment pc ON p.id = pc.post_id WHERE p.title LIKE :titlePattern ORDER BY p.created_on ) p_pc ) p_pc_r WHERE p_pc_r.rank <= :rank
Однако мы не хотим возвращать список значений скалярных столбцов. Мы хотим вернуть сущности JPA из этого запроса, поэтому нам нужно настроить атрибут entities
аннотации @SqlResultSetMapping
, как это:
@NamedNativeQuery( name = "PostWithCommentByRank", query = """ SELECT * FROM ( SELECT *, DENSE_RANK() OVER ( ORDER BY "p.created_on", "p.id" ) rank FROM ( SELECT p.id AS "p.id", p.created_on AS "p.created_on", p.title AS "p.title", pc.post_id AS "pc.post_id", pc.id as "pc.id", pc.created_on AS "pc.created_on", pc.review AS "pc.review" FROM post p LEFT JOIN post_comment pc ON p.id = pc.post_id WHERE p.title LIKE :titlePattern ORDER BY p.created_on ) p_pc ) p_pc_r WHERE p_pc_r.rank <= :rank """, resultSetMapping = "PostWithCommentByRankMapping" ) @SqlResultSetMapping( name = "PostWithCommentByRankMapping", entities = { @EntityResult( entityClass = Post.class, fields = { @FieldResult(name = "id", column = "p.id"), @FieldResult(name = "createdOn", column = "p.created_on"), @FieldResult(name = "title", column = "p.title"), } ), @EntityResult( entityClass = PostComment.class, fields = { @FieldResult(name = "id", column = "pc.id"), @FieldResult(name = "createdOn", column = "pc.created_on"), @FieldResult(name = "review", column = "pc.review"), @FieldResult(name = "post", column = "pc.post_id"), } ) } )
С помощью SqlResultSetMapping
на месте мы можем получить Сообщение
и Комментарий к сообщению
сущности, подобные этой:
List
И мы можем проверить, правильно ли выбраны объекты:
assertEquals( POST_RESULT_COUNT * COMMENT_COUNT, postAndCommentList.size() ); for (int i = 0; i < COMMENT_COUNT; i++) { Post post = (Post) postAndCommentList.get(i)[0]; PostComment comment = (PostComment) postAndCommentList.get(i)[1]; assertTrue(entityManager.contains(post)); assertTrue(entityManager.contains(comment)); assertEquals( "High-Performance Java Persistence - Chapter 1", post.getTitle() ); assertEquals( String.format( "Comment nr. %d - A must read!", i + 1 ), comment.getReview() ); }
@EntityResult
также полезен при извлечении сущностей JPA с помощью хранимых процедур SQL. Ознакомьтесь с этой статьей для получения более подробной информации.
JPA SqlResultSetMapping – конструкторрезультат
Предположим, что мы хотим выполнить запрос агрегирования, который подсчитывает количество записей post_comment
для каждой записи
и возвращает запись
заголовок
для целей отчетности. Для достижения этой цели мы можем использовать следующий SQL-запрос:
SELECT p.id AS "p.id", p.title AS "p.title", COUNT(pc.*) AS "comment_count" FROM post_comment pc LEFT JOIN post p ON p.id = pc.post_id GROUP BY p.id, p.title ORDER BY p.id
Мы также хотим инкапсулировать заголовок поста и количество комментариев в следующем:
public class PostTitleWithCommentCount { private final String postTitle; private final int commentCount; public PostTitleWithCommentCount( String postTitle, int commentCount) { this.postTitle = postTitle; this.commentCount = commentCount; } public String getPostTitle() { return postTitle; } public int getCommentCount() { return commentCount; } }
Чтобы сопоставить результирующий набор приведенного выше SQL-запроса с Заголовком публикации с количеством комментариев
Кроме того, мы можем использовать атрибут classes
аннотации @SqlResultSetMapping
, например:
@NamedNativeQuery( name = "PostTitleWithCommentCount", query = """ SELECT p.id AS "p.id", p.title AS "p.title", COUNT(pc.*) AS "comment_count" FROM post_comment pc LEFT JOIN post p ON p.id = pc.post_id GROUP BY p.id, p.title ORDER BY p.id """, resultSetMapping = "PostTitleWithCommentCountMapping" ) @SqlResultSetMapping( name = "PostTitleWithCommentCountMapping", classes = { @ConstructorResult( columns = { @ColumnResult(name = "p.title"), @ColumnResult(name = "comment_count", type = int.class) }, targetClass = PostTitleWithCommentCount.class ) } )
Аннотация Результат конструктора
позволяет нам указать Hibernate, какой класс DTO использовать, а также какой конструктор вызывать при создании экземпляров объектов DTO.
Обратите внимание, что мы использовали атрибут type
аннотации @ColumnResult
, чтобы указать, что comment_count
должен быть приведен к Java int
. Это необходимо, так как некоторые драйверы JDBC используют либо Long
, либо BigInteger
для результатов функции агрегирования SQL.
Вот как вы можете вызвать Заголовок сообщения С количеством комментариев
именованный собственный запрос с помощью JPA:
ListpostTitleAndCommentCountList = entityManager .createNamedQuery("PostTitleWithCommentCount") .setMaxResults(POST_RESULT_COUNT) .getResultList();
И мы видим, что возвращенный Заголовок сообщения С количеством комментариев
DTO был выбран правильно:
assertEquals(POST_RESULT_COUNT, postTitleAndCommentCountList.size()); for (int i = 0; i < POST_RESULT_COUNT; i++) { PostTitleWithCommentCount postTitleWithCommentCount = postTitleAndCommentCountList.get(i); assertEquals( String.format( "High-Performance Java Persistence - Chapter %d", i + 1 ), postTitleWithCommentCount.getPostTitle() ); assertEquals(COMMENT_COUNT, postTitleWithCommentCount.getCommentCount()); }
Для получения более подробной информации о наилучшем способе получения прогнозов с помощью JPA и гибернации ознакомьтесь с этой статьей .
JPA SqlResultSetMapping – ColumnResult
Предыдущий пример показал, как мы могли бы сопоставить результирующий набор агрегации SQL с DTO. Но что, если мы хотим вернуть объект JPA, для которого мы подсчитываем комментарии?
Для достижения этой цели мы можем использовать атрибут entities
для определения Post
сущности, которую мы извлекаем, и атрибут classes
аннотации @SqlResultSetMapping
для отображения скалярного значения, которое в нашем случае является числом связанных post_comment
записей:
@NamedNativeQuery( name = "PostWithCommentCount", query = """ SELECT p.id AS "p.id", p.title AS "p.title", p.created_on AS "p.created_on", COUNT(pc.*) AS "comment_count" FROM post_comment pc LEFT JOIN post p ON p.id = pc.post_id GROUP BY p.id, p.title ORDER BY p.id """, resultSetMapping = "PostWithCommentCountMapping" ) @SqlResultSetMapping( name = "PostWithCommentCountMapping", entities = @EntityResult( entityClass = Post.class, fields = { @FieldResult(name = "id", column = "p.id"), @FieldResult(name = "createdOn", column = "p.created_on"), @FieldResult(name = "title", column = "p.title"), } ), columns = @ColumnResult( name = "comment_count", type = int.class ) )
При выполнении Сообщения С количеством комментариев
именованный собственный запрос:
List
мы получим как Запись
сущность, так и количество комментариев
значение скалярного столбца:
assertEquals(POST_RESULT_COUNT, postWithCommentCountList.size()); for (int i = 0; i < POST_RESULT_COUNT; i++) { Post post = (Post) postWithCommentCountList.get(i)[0]; int commentCount = (int) postWithCommentCountList.get(i)[1]; assertTrue(entityManager.contains(post)); assertEquals(i + 1, post.getId().intValue()); assertEquals( String.format( "High-Performance Java Persistence - Chapter %d", i + 1 ), post.getTitle() ); assertEquals(COMMENT_COUNT, commentCount); }
Вывод
JPA предлагает несколько способов отображения результирующего набора данного запроса. Вы можете использовать выражение конструктора JPQL или получить результаты в виде Кортежа
.
Однако аннотация SqlResultSetMapping
является наиболее гибким подходом, поскольку вы можете использовать ее для извлечения DTO, сущностей или значений скалярных столбцов.
Хотя использование этого в качестве API, аналогичного Hibernate ResultTransformer , было бы гораздо лучшим способом построения сопоставления набора результатов SQL по сравнению с декларативным подходом, заданным аннотацией @SqlResultSetMapping
, пока спецификация JPA не предоставит программный подход, вы можете использовать аннотацию @SqlResultSetMapping
для этой задачи.