如何正确地使用JPA 2 CriteriaQuery来表达JPQL中的"join fetch"和"where"子句?

69
考虑以下的JPQL查询:
SELECT foo FROM Foo foo
INNER JOIN FETCH foo.bar bar
WHERE bar.baz = :baz

我正在尝试将这个转化为Criteria查询。目前为止,我只完成了以下部分:
var cb = em.getCriteriaBuilder();
var query = cb.createQuery(Foo.class);
var foo = query.from(Foo.class);
var fetch = foo.fetch(Foo_.bar, JoinType.INNER);
var join = foo.join(Foo_.bar, JoinType.INNER);
query.where(cb.equal(join.get(Bar_.baz), value);

显而易见的问题是我在做两次相同的连接,因为Fetch类似乎没有获取Path的方法。 有没有办法避免两次连接?还是我必须坚持使用简单的JPQL查询?

1
好的,谢谢,但我更愿意坚持使用标准API并尽量避免使用额外的第三方库。如果JPA Criteria API无法实现我想要的功能,我可能会坚持使用普通的JPQL。 - chris
你解决了你的问题吗?我也遇到了同样的问题。Fetch无法转换为Join,我也无法从Fetch中获取Path。它几乎是无法使用的。唯一的解决方案是拥有两个相同的Join,这是不可接受的。 - svlada
4
好的,詹姆斯的回答很好地描述了问题的根源。你不能这样做,这是一个合理的设计决策。如果我没记错,我最终实际上加入了两次。 话虽如此,如果可以选择,我将永远不会再使用JPA,因为我认为它是一个无用的抽象层,增加了不必要的复杂性,同时也削弱了底层的实现。 - chris
3个回答

97
在JPQL中,规范实际上也是如此。JPA规范不允许给Fetch join指定别名。问题在于,通过限制join fetch的上下文,您很容易自己搞砸。最安全的方法是两次加入(join twice)。这通常更多地涉及ToMany而不是ToOne。例如,
Select e from Employee e 
join fetch e.phones p 
where p.areaCode = '613'

这样做会错误地返回所有电话号码中包含'613'区号的员工,但会在返回列表中省略其他区域的电话号码。这意味着如果一个员工有613和416区号的电话号码,将会失去416的电话号码,从而导致对象被破坏。

尽管不希望进行额外的连接,如果您知道自己在做什么,一些JPA提供程序可能允许别名加入提取,并且可能允许将Criteria Fetch强制转换为Join。


4
非常棒的答案,谢谢。我没有想到这一点。Hibernate从来没有向我抱怨过关于别名的fetch join,我不知道这实际上违反了规范。 - chris
5
另请参阅:http://java-persistence-performance.blogspot.com/2012/04/objects-vs-data-and-filtering-join.html - James
我正在进行这种类型的查询,并想知道在这种确切情况下结果会是什么样子。这解决了我的困惑。 - Faraway
我发现这个答案是不正确的。在我的情况下,像这样的查询仅返回具有所有电话的员工,其areaCode = '613'。它并没有像你说的那样过滤电话集合。 - pavlee
1
Join fetch 是 Join 的重点。当嵌套的集合被过滤掉时,对象不会被破坏 - 无论是用户代码还是 JPA join fetch。 - Anton Pryamostanov
嗯...最终,我按照自己的需求使用了这个答案中“错误”的部分 :) 但是我的问题是-当我添加一个以上的join fetch时,相同的电话号码和613区域会被多次添加(与新添加的join fetch数量完全相同)... - RAM237

37

我将使用与詹姆斯答案相同的示例并添加另一种解决方案,直观地展示问题。

如果您执行以下查询但没有使用FETCH

Select e from Employee e 
join e.phones p 
where p.areaCode = '613'

您将从Employee获得所期望的以下结果:

员工ID 员工姓名 电话ID 电话区号
1 詹姆斯 5 613
1 詹姆斯 6 416

但是,当您在JOINFETCH JOIN)上添加FETCH子句时,会发生以下情况:

员工ID 员工姓名 电话ID 电话区号
1 詹姆斯 5 613

生成的 SQL 对于这两个查询是相同的,但是当你在 FETCH 连接上使用 WHERE 时,Hibernate 会从内存中删除 416 的记录。

因此,为了获取所有手机并正确应用 WHERE,您需要��两个 JOIN:一个用于 WHERE,另一个用于 FETCH。像这样:

Select e from Employee e 
join e.phones p 
join fetch e.phones      //no alias, to not commit the mistake
where p.areaCode = '613'

也许在最新版本的Hibernate中,您需要使用SELECT DISTINCT来避免重复结果。


3
第一个查询会返回区号为613和416的员工,原因是在查询中有一个where子句,如果我理解正确的话,那么Employee和Phone之间会进行内部连接。然后为什么会返回613和416呢?这是因为内部连接将两个表中相关联的行匹配起来,在这种情况下,Employee表中有员工的电话号码对应着两个不同的区号,即613和416,因此查询结果会包含这两个区号的员工。 - Faizan
1
@FaizanAhmad 第一个查询仅返回员工实体“James”,因为他的两个地址中有一个代码为613。当使用fetch连接时,一些开发人员希望也能收到这两个地址,因为其中一个是613,但是Hibernate会在内存中过滤并排除416,仅返回613。因此,您需要一个fetch连接(将所有地址带来)和另一个where条件(返回具有某些地址的613代码的任何员工)的连接。 - Dherik
我仍然感到困惑,让我告诉您我心中所想的 SQL。对于第一种情况,Select * from Employees as e inner join Phones as p on e.employeeId = p.employeeId where p.areacode = 613,如果这是第一种情况的查询,我们将得不到区号为“416”的条目。 - Faizan
1
很抱歉,从性能角度来看,这个解决方案非常糟糕,因为它将产生n^2行,其中n是集合的大小。 - fantaztig
2
@FaizanAhmad 如果我们只考虑纯SQL的话,你是对的。但是在这里,我们选择的是实体,它们可以“嵌入”关系。Employee实体包含员工所有电话的列表(通常是OneToMany关联)。因此,当我们选择整个员工实体(e)时,无论where子句是什么,它都应该包含该员工的所有电话。问题在于,当使用fetch时,Hibernate会在内部从e.phones列表中排除不在where子句中的值。 - Yann39
1
使用最新版本,我们会得到一个重复的结果行,使用这个解决方案可以修复它。如果您能更新您的答案,那将非常棒。请使用 SELECT DISTINCT ... - Sunchezz

3

可能我回答晚了,但从我的角度来看。

Select e from Employee e 
join e.phones p 
join fetch e.phones      //no alias, to not commit the mistake
where p.areaCode = '613'

这可以被翻译成以下的SQL查询

Select e.id, e.name, p.id ,p.phone
From Employe e
inner join Phone p on e.id = p.emp_id
where exists(
  select 1 from Phone where Phone.id= p.id and Phone.area ='XXX'  
)

这将获取属于某个区域的员工的所有电话。

但是

Select e from Employee e 
join fetch e.phones p      //no alias, to not commit the mistake
where p.areaCode = '613'

可以被翻译成以下的SQL查询语句

Select  e.id, e.name, p.id ,p.phone
From    Employe e
inner   join Phone p on e.id = p.id
Where   p.area ='XXX'  

或者

Select e.id, e.name, p.id ,p.phone
From Employe e
inner join Phone p on e.id = p.emp_id and p.area ='XXX'  

这将限制行选择仅限于员工电话号码属于XXX地区的行。

最后写下这个。

Select e from Employee e 
join  e.phones p      
where p.areaCode = '613'

可以看作是

Select e.id, e.name 
from Employe e
where exists (
 select 1 from phone p where p.emp_id = e.id and p.area = 'XXX'
)

我们只获取在某个地区有电话号码的员工数据

这应该有助于在每次查询之后理解思路。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接