Модель домена
Предположим, что в нашей базе данных есть следующие таблицы post
и post_comment
:
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 для регистрации сопоставления, есть три варианта сопоставления:
EntityResult
ConstructorResult
ColumnResult
Далее мы рассмотрим, как работают все эти три варианта сопоставления, а также варианты использования, в которых вам нужно будет их использовать.
JPA SqlResultSetMapping — EntityResult
Параметр EntityResult
позволяет сопоставить столбцы JDBC ResultSet
с одним или несколькими объектами JPA.
Предположим, мы хотим получить первые 5 объектов Post
вместе со всеми связанными с ними объектами PostComment
, которые соответствуют заданному шаблону title
.
Как я объяснял в этой статье, мы можем использовать 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
, мы можем получить объекты Post
и PostComment
следующим образом:
List<Object[]> postAndCommentList = entityManager
.createNamedQuery("PostWithCommentByRank")
.setParameter("titlePattern", "High-Performance Java Persistence %")
.setParameter("rank", POST_RESULT_COUNT)
.getResultList();
И мы можем проверить правильность выборки объектов:
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 — ConstructorResult
Предположим, мы хотим выполнить запрос агрегации, который подсчитывает количество post_coment
записей для каждого post
и возвращает post
title
для целей отчетности. Мы можем использовать следующий 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
Мы также хотим инкапсулировать заголовок сообщения и количество комментариев в следующем DTO:
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-запроса с PostTitleWithCommentCount
DTO, мы можем использовать атрибут 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
)
}
)
Аннотация ConstructorResult
позволяет указать Hibernate, какой класс DTO использовать, а также какой конструктор вызывать при создании объектов DTO.
Обратите внимание, что мы использовали атрибут type
аннотации @ColumnResult
, чтобы указать, что comment_count
следует преобразовать в Java int
. Это необходимо, поскольку некоторые драйверы JDBC используют либо Long
, либо BigInteger
для результатов функции агрегирования SQL.
Вот как вы можете вызвать собственный запрос с именем PostTitleWithCommentCount
с помощью JPA:
List<PostTitleWithCommentCount> postTitleAndCommentCountList = entityManager
.createNamedQuery("PostTitleWithCommentCount")
.setMaxResults(POST_RESULT_COUNT)
.getResultList();
И мы видим, что возвращенные PostTitleWithCommentCount
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());
}
Дополнительные сведения о том, как лучше всего получать проекции DTO с помощью JPA и Hibernate, см. на странице этой статьи.
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
)
)
При выполнении нативного запроса с именем PostWithCommentCount
:
List<Object[]> postWithCommentCountList = entityManager
.createNamedQuery("PostWithCommentCount")
.setMaxResults(POST_RESULT_COUNT)
.getResultList();
мы получим как сущность Post
, так и скалярное значение столбца commentCount
:
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);
}
person
Vlad Mihalcea
schedule
24.03.2020