Как правильно выразить JPQL "join fetch" ​​с предложением "where" как JPA 2 CriteriaQuery?

Рассмотрим следующий запрос JPQL:

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

Я пытаюсь перевести это в запрос Critieria. Это до тех пор, пока я не получил:

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

Очевидная проблема заключается в том, что я делаю одно и то же соединение дважды, потому что Fetch<Foo, Bar>, похоже, не имеет метода для получения Path. Есть ли способ избежать необходимости вступать дважды? Или я должен придерживаться старого старого JPQL с таким простым запросом?

Ответ 1

В JPQL то же самое действительно верно в спецификации. Спецификация JPA не разрешает давать псевдоним соединению выборки. Проблема в том, что вы можете легко выстрелить себе в ногу с этим, ограничив контекст выборки соединения. Безопаснее присоединиться дважды.

Это обычно больше проблема с ToMany, чем ToOnes. Например,

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

Это приведет к неправильному возвращению всех Сотрудников, которые содержат номера в коде области "613", но пропустит номера телефонов других областей в возвращенном списке. Это означает, что сотрудник, у которого был телефон с кодами 613 и 416, потеряет номер телефона 416, поэтому объект будет поврежден.

Конечно, если вы знаете, что делаете, дополнительное объединение нежелательно, некоторые провайдеры JPA могут разрешить использование псевдонима для выборки объединения и могут разрешить приведение Criteria Fetch к соединению.

Ответ 2

Я наглядно продемонстрирую проблему, используя отличный пример ответа Джеймса и добавив альтернативное решение.

Когда вы делаете следующий запрос, без FETCH:

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

Вы получите следующие результаты от Employee как вы ожидали:

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

Но когда вы добавляете слово FETCH в JOIN, вот что происходит:

EmployeeId | EmployeeName | PhoneId | PhoneAreaCode
1          | James        | 5       | 613

Сгенерированный SQL одинаков для двух запросов, но Hibernate удаляет из памяти регистр 416 когда вы используете WHERE при соединении FETCH.

Таким образом, чтобы собрать все телефоны и правильно применить 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'