Введение
В этой статье я покажу вам, как писать подзапросы EXISTS с помощью JPA и Hibernate.
Подзапросы EXISTS очень полезны, так как позволяют реализовать SemiJoins. К сожалению, многие разработчики приложений не знают о SemiJoins и ограничиваются тем, что эмулируют его с помощью EquiJoins (например, INNER JOIN) в ущерб производительности запросов.
Модель домена
Предположим, мы используем следующие объекты Post
и PostComment
:
Объект Post
является родительским, а объект PostComment
является дочерним, поскольку PostComment
ссылается на родительский объект через его свойство post
.
Извлечение родительских сущностей при фильтрации по дочерним сущностям
Давайте предположим, что мы хотим получить все сущности Post
, у которых имеется PostComent
со значением score больше 10. Большинство разработчиков ошибочно воспользуются следующим запросом:
List<Post> posts = entityManager.createQuery("""
select distinct p
from PostComment pc
join pc.post p
where pc.score > :minScore
order by p.id
""", Post.class)
.setParameter("minScore", 10)
.setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
.getResultList();
Этот запрос выполняет соединение между post
и post_comment
только для фильтрации записей post
. Поскольку проекция содержит только объект Post
, в этом случае JOIN не требуется. Вместо этого для фильтрации записей сущностей следует использовать SemiJoin Post
.
Используется
HINT_PASS_DISTINCT_THROUGH
для предотвращения передачи ключевого словаDISTINCT
в базовый SQL-запрос, поскольку дедупликация выполняется для ссылок на объекты Java, а не для записей таблицы SQL. Ознакомьтесь с этой статьей для более подробной информации по этой теме.
Подзапросы EXISTS с JPQL
Как я объяснял в этой статье, подзапрос EXISTS — гораздо лучшая альтернатива. Следовательно, мы можем достичь нашей цели, используя следующий запрос JPQL:
List<Post> posts = entityManager.createQuery("""
select p
from Post p
where exists (
select 1
from PostComment pc
where
pc.post = p and
pc.score > :minScore
)
order by p.id
""", Post.class)
.setParameter("minScore", 10)
.getResultList();
При выполнении приведенного выше запроса JPQL Hibernate генерирует следующий запрос SQL:
SELECT
p.id AS id1_0_,
p.title AS title2_0_
FROM post p
WHERE EXISTS (
SELECT 1
FROM post_comment pc
WHERE
pc.post_id=p.id AND
pc.score > ?
)
ORDER BY p.id
Преимущество этого запроса в том, что SemiJoin не нужно объединять все записи post
и post_comment
, поскольку, как только post_comment
найдено соответствие критериям фильтрации (например, pc.score > ?
), EXISTS
предложение возвращается, true
и запрос переходит к следующей записи post
.
Подзапросы EXISTS с Criteria API
Если вы хотите динамически построить запрос объекта, вы можете использовать Criteria API, поскольку, как и JPQL, он поддерживает фильтрацию подзапросов.
Предыдущий запрос JPQL можно переписать в запрос Criteria API, например так:
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Post> query = builder.createQuery(Post.class);
Root<Post> p = query.from(Post.class);
ParameterExpression<Integer> minScore = builder.parameter(Integer.class);
Subquery<Integer> subQuery = query.subquery(Integer.class);
Root<PostComment> pc = subQuery.from(PostComment.class);
subQuery
.select(builder.literal(1))
.where(
builder.equal(pc.get(PostComment_.POST), p),
builder.gt(pc.get(PostComment_.SCORE), minScore)
);
query.where(builder.exists(subQuery));
List<Post> posts = entityManager.createQuery(query)
.setParameter(minScore, 10)
.getResultList();
Приведенный выше запрос Criteria API генерирует тот же SQL-запрос, что и предыдущий запрос JPQL.
Подзапросы EXISTS с Blaze Persistence
Если вы не являетесь большим поклонником Criteria API, то есть гораздо лучшая альтернатива построению динамических запросов сущностей. Blaze Persistence позволяет писать динамические запросы, которые не только более удобочитаемы, но и более эффективны, поскольку вы можете использовать LATERAL JOIN , Derived Tables , Common Table Expressions или Window Functions .
Предыдущий запрос с Criteria API можно переписать с использованием Blaze Persistence, например так:
final String POST_ALIAS = "p";
final String POST_COMMENT_ALIAS = "pc";
List<Post> posts = cbf.create(entityManager, Post.class)
.from(Post.class, POST_ALIAS)
.whereExists()
.from(PostComment.class, POST_COMMENT_ALIAS)
.select("1")
.where(PostComment_.POST).eqExpression(POST_ALIAS)
.where(PostComment_.SCORE).gtExpression(":minScore")
.end()
.select(POST_ALIAS)
.setParameter("minScore", 10)
.getResultList();
При выполнении запроса Blaze Persistence выше Hibernate будет генерировать тот же оператор SQL, который был создан вышеупомянутыми запросами JPQL или Criteria API.
Потрясающе, правда?
Заключение
SemiJoins очень полезны для фильтрации, и вы должны предпочесть их EquiJoins, когда проекция запроса не содержит ни одного из соединенных столбцов.
В SQL SemiJoins выражаются с помощью подзапросов EXISTS, и эта функция не ограничивается собственными SQL-запросами, поскольку вы можете использовать EXISTS в запросах сущностей JPA и Hibernate как с JPQL, так и с Criteria API, а также с запросами Blaze Persistence.