如何用JPA和Hibernate编写EXISTS子查询

1,728 阅读3分钟

简介

在这篇文章中,我将告诉你如何用JPA和Hibernate编写EXISTS子查询。

EXISTS子查询是非常有用的,因为它允许你实现SemiJoins。不幸的是,许多应用程序开发人员并不了解SemiJoins,他们最终使用EquiJoins(如INNER JOIN)来模仿它,但却牺牲了查询性能。

领域模型

让我们假设我们正在使用以下PostPostComment 实体。

Post and PostComment entitiesPost and PostComment entities

Post 实体是父实体,PostComment 是子实体,因为PostComment 通过其post 属性引用了父实体。

在通过子实体过滤的同时获取父实体

让我们假设我们想获取所有Post 实体,这些实体有一个得分大于10的PostComent 。大多数开发者会错误地使用下面的查询。

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();

这个查询在postpost_comment 之间执行一个连接,只是为了过滤post 的记录。由于投影只包含Post 实体,在这种情况下不需要JOIN。相反,应该使用SemiJoin来过滤Post 实体记录。

HINT_PASS_DISTINCT_THROUGH 是用来防止DISTINCT 关键字被传递给底层的SQL查询,因为重复数据删除是针对Java对象引用,而不是SQL表记录。请查看这篇文章,了解关于这个主题的更多细节。

使用JPQL的EXISTS子查询

正如我在篇文章中解释的,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不需要连接所有的postpost_comment 记录,因为只要发现一个post_comment 匹配过滤条件(例如:pc.score > ? ),EXISTS 子句就会返回true ,查询就会继续到下一个post 记录。

使用Criteria API的EXISTS子查询

如果你想动态地建立实体查询,那么你可以使用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查询生成的SQL查询完全相同。

使用Blaze Persistence的EXISTS子查询

如果你不太喜欢Criteria API,那么还有一个更好的办法来构建动态实体查询。Blaze Persistence允许你编写动态查询,不仅可读性更强,而且更强大,因为你可以使用LATERAL JOIN衍生表通用表表达式窗口函数

之前的Criteria API查询可以用Criteria API重写,像这样。

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将生成与上述JPQL或Criteria API查询所生成的相同的SQL语句。

棒极了,对吗?

结论

SemiJoins对于过滤来说非常有用,当查询投影不包含任何连接的列时,你应该更喜欢它们而不是EquiJoins。

在SQL中,SemiJoins是用EXISTS子查询来表达的,这个功能并不局限于本地SQL查询,因为你可以在JPA和Hibernate实体查询中使用EXISTS,同时使用JPQL和Criteria API以及Blaze Persistence查询。