简介
在这篇文章中,我将告诉你如何用JPA和Hibernate编写EXISTS子查询。
EXISTS子查询是非常有用的,因为它允许你实现SemiJoins。不幸的是,许多应用程序开发人员并不了解SemiJoins,他们最终使用EquiJoins(如INNER JOIN)来模仿它,但却牺牲了查询性能。
领域模型
让我们假设我们正在使用以下Post 和PostComment 实体。


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();
这个查询在post 和post_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不需要连接所有的post 和post_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查询。