Ответ 3
Несмотря на то, что вы получаете достоверные результаты, SQL-запрос извлекает все данные, и он не так эффективен, как следовало бы.
Как я объяснил в этой статье, у вас есть два варианта.
Исправление проблемы с двумя запросами SQL, которые могут извлекать объекты в режиме чтения-записи
Самый простой способ решить эту проблему - выполнить два запроса:
Первый запрос извлекает идентификаторы корневого объекта, соответствующие предоставленным критериям фильтрации. , Второй запрос будет использовать ранее извлеченные идентификаторы корневых объектов для извлечения родительских и дочерних объектов.
Этот подход очень прост в реализации и выглядит следующим образом:
List<Long> postIds = entityManager
.createQuery(
"select p.id " +
"from Post p " +
"where p.title like :titlePattern " +
"order by p.createdOn", Long.class)
.setParameter(
"titlePattern",
"High-Performance Java Persistence %"
)
.setMaxResults(5)
.getResultList();
List<Post> posts = entityManager
.createQuery(
"select distinct p " +
"from Post p " +
"left join fetch p.comments " +
"where p.id in (:postIds)", Post.class)
.setParameter("postIds", postIds)
.setHint(
QueryHints.HINT_PASS_DISTINCT_THROUGH,
false
)
.getResultList();
Исправление проблемы с одним запросом SQL, который может извлекать объекты только в режиме только для чтения
Второй подход заключается в использовании SDENSE_RANK для набора результатов родительских и дочерних объектов, которые соответствуют нашим критериям фильтрации и ограничивают вывод только для первых N записей записей.
Запрос SQL может выглядеть следующим образом:
@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.id as \"pc.id\", " +
" pc.created_on AS \"pc.created_on\", " +
" pc.review AS \"pc.review\", " +
" pc.post_id AS \"pc.post_id\" " +
" 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"),
}
)
}
)
@NamedNativeQuery
выбирает все сущности Post, соответствующие предоставленному заголовку, вместе со связанными с ними дочерними сущностями PostComment
. Оконная функция DENSE_RANK
используется для назначения ранга каждой объединенной записи Post и PostComment
чтобы впоследствии мы могли отфильтровать только то количество записей Post, которые нам интересны.
SqlResultSetMapping
обеспечивает сопоставление между псевдонимами столбцов уровня SQL и свойствами сущностей JPA, которые необходимо заполнить.
Теперь мы можем выполнить PostWithCommentByRank
@NamedNativeQuery
следующим образом:
List<Post> posts = entityManager
.createNamedQuery("PostWithCommentByRank")
.setParameter(
"titlePattern",
"High-Performance Java Persistence %"
)
.setParameter(
"rank",
5
)
.unwrap(NativeQuery.class)
.setResultTransformer(
new DistinctPostResultTransformer(entityManager)
)
.getResultList();
Теперь, по умолчанию, собственный SQL-запрос, такой как PostWithCommentByRank
будет PostWithCommentByRank
Post и PostComment
в одной и той же строке JDBC, поэтому мы получим Object [], содержащий обе сущности.
Однако мы хотим преобразовать табличный массив Object[]
в дерево родительских и дочерних объектов, и по этой причине нам нужно использовать Hibernate ResultTransformer
Для получения дополнительной информации о ResultTransformer
ознакомьтесь с этой статьей.
DistinctPostResultTransformer
выглядит следующим образом:
public class DistinctPostResultTransformer
extends BasicTransformerAdapter {
private final EntityManager entityManager;
public DistinctPostResultTransformer(
EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public List transformList(
List list) {
Map<Serializable, Identifiable> identifiableMap =
new LinkedHashMap<>(list.size());
for (Object entityArray : list) {
if (Object[].class.isAssignableFrom(entityArray.getClass())) {
Post post = null;
PostComment comment = null;
Object[] tuples = (Object[]) entityArray;
for (Object tuple : tuples) {
if(tuple instanceof Identifiable) {
entityManager.detach(tuple);
if (tuple instanceof Post) {
post = (Post) tuple;
}
else if (tuple instanceof PostComment) {
comment = (PostComment) tuple;
}
else {
throw new UnsupportedOperationException(
"Tuple " + tuple.getClass() + " is not supported!"
);
}
}
}
if (post != null) {
if (!identifiableMap.containsKey(post.getId())) {
identifiableMap.put(post.getId(), post);
post.setComments(new ArrayList<>());
}
if (comment != null) {
post.addComment(comment);
}
}
}
}
return new ArrayList<>(identifiableMap.values());
}
}
DistinctPostResultTransformer
должен отсоединить извлекаемые сущности, потому что мы перезаписываем дочернюю коллекцию и не хотим, чтобы это передавалось как переход состояния сущности:
post.setComments(new ArrayList<>());
Для более подробной информации, ознакомьтесь с этой статьей.