Как правильно выразить выборку соединения JPQL с предложением where как JPA 2 CriteriaQuery?

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

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

Я пытаюсь перевести это в запрос Criteria. Это насколько я понял:

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 с таким простым запросом?


person chris    schedule 28.04.2011    source источник
comment
Хорошо, спасибо, но я бы предпочел придерживаться стандартных API и стараться избегать дополнительных сторонних библиотек. Если то, что я хочу сделать, невозможно с помощью JPA Criteria API, я, вероятно, просто буду придерживаться простого JPQL.   -  person chris    schedule 28.04.2011
comment
Вы решили свою проблему? У меня такая же проблема. Fetch не может быть приведен к Join, и я не могу получить Path от Fetch. Это практически непригодно. Единственное решение состоит в том, чтобы иметь два одинаковых соединения, что неприемлемо.   -  person svlada    schedule 22.04.2015
comment
Что ж, ответ Джеймса довольно хорошо описывает корень проблемы. Вы просто не можете сделать это таким образом, и это разумное дизайнерское решение. Если я правильно помню, я фактически присоединился дважды. При этом я никогда больше не буду использовать JPA, если у меня будет выбор, потому что я думаю, что это бесполезный уровень абстракции, который добавляет ненужную сложность, кастрируя базовую реализацию.   -  person chris    schedule 22.04.2015


Ответы (3)


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

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

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

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

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

person James    schedule 28.04.2011
comment
Блестящий ответ, спасибо. Не думал об этом. Hibernate никогда не жаловался мне на псевдоним объединения выборки, я не знал, что это фактически нарушает спецификацию. - person chris; 28.04.2011
comment
См. также java-persistence. -performance.blogspot.com/2012/04/ - person James; 24.04.2012
comment
Я выполнял именно такой запрос и задавался вопросом, каковы будут результаты в этом конкретном случае. Это решает мое замешательство. - person Faraway; 30.05.2018
comment
Я считаю этот ответ неверным. В моем случае такой запрос возвращает только сотрудников, у которых есть все телефоны с areaCode = '613'. Он не фильтрует коллекцию телефонов, как вы сказали. - person pavlee; 29.10.2018
comment
Присоединиться к извлечению - это весь смысл присоединения. Объект не повреждается, когда его вложенные коллекции отфильтровываются, независимо от того, является ли это кодом пользователя или выборкой соединения JPA. - person Anton Pryamostanov; 09.05.2020

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

Когда вы выполняете следующий запрос без 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'
person Dherik    schedule 04.07.2018
comment
Не могли бы вы немного объяснить это с помощью SQL-запроса, почему первый запрос вернет сотрудников с кодом области 613 и 416, потому что есть предложение where, и если я правильно его понимаю, между сотрудником и телефоном будет внутреннее соединение. Тогда почему он вернет 613 и 416? Разве он не должен возвращать только 613? - person Faizan Ahmad; 25.02.2021
comment
@FaizanAhmad первый запрос вернет только сущность сотрудника Джеймс, потому что один из двух его адресов имеет код 613. Когда вы используете соединение fetch, некоторые разработчики ожидают также получить эти два адреса, потому что один из них 613, но спящий фильтр в памяти и исключает 416 из результата, возвращая только 613. Итак, вам нужно одно соединение для fetch (чтобы привести все адреса) и еще один для условия where (вернуть любого сотрудника, у которого есть адрес с кодом 613). - person Dherik; 25.02.2021
comment
Я все еще в замешательстве. Позвольте мне сказать вам, какой SQL я имею в виду. Для случая один Select * from Employees as e inner join Phones as p on e.employeeId = p.employeeId where p.areacode = 613 , если это запрос для случая один, мы не получим запись с кодом области 416 - person Faizan Ahmad; 26.02.2021
comment
Извините, но с точки зрения производительности это решение просто ужасно, поскольку оно будет создавать n ^ 2 строки, где n - это размер коллекции. - person fantaztig; 02.04.2021

Я могу ответить поздно, но с моей точки зрения.

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'  

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'  

это ограничит выбор строк только теми строками, в которых телефон сотрудников относится к области 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'
)

Где мы получаем только данные о сотрудниках, у которых есть номер телефона в какой-то области

Это должно помочь понять идею после каждого запроса.

person Mohammed Housseyn Taleb    schedule 17.06.2021