如何正确地使用JPA 2 CriteriaQuery来表达JPQL中的"join fetch"和"where"子句?
如何正确地使用JPA 2 CriteriaQuery来表达JPQL中的"join fetch"和"where"子句?
考虑以下的JPQL查询:
SELECT foo FROM Foo foo INNER JOIN FETCH foo.bar bar WHERE bar.baz = :baz
我正在尝试将其翻译成Criteria查询。目前为止,我已经做到了这一点:
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuerycq = cb.createQuery(Foo.class); Root r = cq.from(Foo.class); Fetch fetch = r.fetch(Foo_.bar, JoinType.INNER); Join join = r.join(Foo_.bar, JoinType.INNER); cq.where(cb.equal(join.get(Bar_.baz), value);
明显的问题是我做了两次相同的连接,因为Fetch
似乎没有获取Path
的方法。
有没有办法避免连接两次?还是我必须坚持使用简单的JPQL查询?
如何正确地使用JPA 2 CriteriaQuery表达JPQL中的“join fetch”与“where”子句?
问题的出现原因是在使用JPA 2 CriteriaQuery时,需要正确地将JPQL中的“join fetch”与“where”子句转换为相应的SQL查询语句。
解决方法是根据不同的JPQL查询,将其转换为相应的SQL查询语句。
第一个JPQL查询是:
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' )
第二个JPQL查询是:
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'
第三个JPQL查询是:
Select e from Employee e
join e.phones p
where p.areaCode = '613'
对应的SQL查询语句是:
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' )
以上就是解决该问题的方法,通过将JPQL查询转换为相应的SQL查询语句,可以正确地使用JPA 2 CriteriaQuery表达JPQL中的“join fetch”与“where”子句。
问题的出现原因是使用JPQL的"join fetch"和"where"子句时,Hibernate会在内存中过滤掉不符合"where"条件的结果,导致查询结果不符合预期。
解决方法是使用两个"JOIN"子句,一个用于"where"条件,另一个用于"fetch"操作。
具体地,我们可以这样写JPQL查询语句:
Select e from Employee e
join e.phones p
join fetch e.phones
where p.areaCode = '613'
这样可以保证查询结果中包含所有的phones,并且正确应用"where"条件。
对于最新版本的Hibernate,可能需要使用"SELECT DISTINCT"来避免重复的结果。
关于为什么第一个查询会返回areaCode为613和416的employee,可以这样解释:第一个查询只返回了employee实体"James",因为他的两个phones中有一个的areaCode为613。当使用"fetch" join时,一些开发人员希望也能收到这两个phones,因为其中一个的areaCode为613,但是Hibernate会在内存中过滤掉areaCode不为613的结果,只返回areaCode为613的结果。因此,我们需要使用两个join子句,一个用于"fetch"操作(获取所有的phones),另一个用于"where"条件(返回任何一个phone的areaCode为613的employee)。
关于性能方面的问题,使用这种解决方法会产生n^2行数据,其中n是集合的大小。在纯SQL的角度来看,你是对的。但是这里我们选择的是查询实体,实体可以"嵌入"关系。Employee实体包含了一个employee的所有phones的列表(通常是OneToMany关联)。所以,当我们选择整个employee实体时(e),无论where子句是什么,它都应该包含该employee的所有phones。问题在于当使用fetch时,Hibernate在内部从e.phones列表中排除了不符合where子句的值。
对于最新版本,使用这种解决方法会产生重复的结果行。可以通过使用"SELECT DISTINCT"来修复这个问题。
问题的出现原因是在JPQL中,JPA规范不允许给fetch join指定一个别名。这个问题在一对多的关系中更为常见。例如,下面的查询语句会返回所有在'613'区号下的电话号码对应的员工,但是会忽略其他区号的电话号码,这会导致员工对象被破坏。如果需要避免这个问题,可以使用两次join。
解决方法是使用两次join,或者有些JPA提供者允许给join fetch指定别名,也可以将Criteria Fetch转换为Join。
参考文章:http://java-persistence-performance.blogspot.com/2012/04/objects-vs-data-and-filtering-join.html
有一位用户表示,他遇到了同样的问题,并且这个解决方法解决了他的困惑。还有一位用户认为这个回答是错误的,他的查询语句只返回了所有电话号码都是'613'区号的员工,而没有过滤掉其他区号的电话号码。还有一位用户表示,他在使用这个解决方法时遇到了问题,当添加了多个join fetch时,相同的电话号码会被多次添加。