How to properly express JPQL "join fetch" with "where" clause as JPA 2 CriteriaQuery?

JavaJpaCriteriaJpa 2.0

Java Problem Overview


Consider the following JPQL query:

SELECT foo FROM Foo foo
INNER JOIN FETCH foo.bar bar
WHERE bar.baz = :baz

I'm trying to translate this into a Criteria query. This is as far as I have gotten:

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Foo> cq = cb.createQuery(Foo.class);
Root<Foo> r = cq.from(Foo.class);
Fetch<Foo, Bar> fetch = r.fetch(Foo_.bar, JoinType.INNER);
Join<Foo, Bar> join = r.join(Foo_.bar, JoinType.INNER);
cq.where(cb.equal(join.get(Bar_.baz), value);

The obvious problem here is that I am doing the same join twice, because Fetch<Foo, Bar> doesn't seem to have a method to get a Path. Is there any way to avoid having to join twice? Or do I have to stick with good old JPQL with a query as simple as that?

Java Solutions


Solution 1 - Java

In JPQL the same is actually true in the spec. The JPA spec does not allow an alias to be given to a fetch join. The issue is that you can easily shoot yourself in the foot with this by restricting the context of the join fetch. It is safer to join twice.

This is normally more an issue with ToMany than ToOnes. For example,

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

This will incorrectly return all Employees that contain numbers in the '613' area code but will left out phone numbers of other areas in the returned list. This means that an employee that had a phone in the 613 and 416 area codes will loose the 416 phone number, so the object will be corrupted.

Granted, if you know what you are doing, the extra join is not desirable, some JPA providers may allow aliasing the join fetch, and may allow casting the Criteria Fetch to a Join.

Solution 2 - Java

I will show visually the problem, using the great example from James answer and adding the alternative solution.

When you do the follow query, without the FETCH:

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

You will have the follow results from Employee as you expected:

EmployeeId EmployeeName PhoneId PhoneAreaCode
1 James 5 613
1 James 6 416

But when you add the FETCH clause on JOIN (FETCH JOIN), this is what happens:

EmployeeId EmployeeName PhoneId PhoneAreaCode
1 James 5 613

The generated SQL is the same for the two queries, but the Hibernate removes on memory the 416 register when you use WHERE on the FETCH join.

So, to bring all phones and apply the WHERE correctly, you need to have two JOINs: one for the WHERE and another for the FETCH. Like:

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

Solution 3 - Java

I may answer late this but from my point of view.

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

This could be translated to the following SQL Query

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'  
)

This will get all phones of an employee that belongs to an area.

BUT

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

could be translated to the following SQL Queries

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'  

or

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'  

this will restrict row selection to only rows where employees phone is of area XXX

And finally writing this

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

Could be seen as

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'
)

Where we are only getting employee data that have a phone number in some area

This should help get the idea after each query.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionchrisView Question on Stackoverflow
Solution 1 - JavaJamesView Answer on Stackoverflow
Solution 2 - JavaDherikView Answer on Stackoverflow
Solution 3 - JavaMohammed Housseyn TalebView Answer on Stackoverflow