Введение

В этой статье я покажу вам, как писать подзапросы 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.

Комментарии (0)