Описание проблемы
Как мы уже сказали выше, у аннотации LazyCollectionOption.EXTRA есть проблема — она выполняет по отдельному запросу к базе для каждого объекта. Нужно как-то объяснить Hibernate, что мы хотим, чтобы он сразу загрузил все дочерние объекты для наших родительских объектов.
Разработчики Hibernate предложили решение этой проблемы — оператор join fetch в HQL.
Пример HQL-запроса:
select distinct task from Task t left join fetch t.employee order by t.deadline
В этом запросе все просто и сложно одновременно. Давай попробуем сконструировать его по частям.
Вариант 1
Мы хотим загрузить все объекты Task, отсортированные по deadline. Вот как будет выглядеть этот запрос:
select task from Task t order by t.deadline
Пока все понятно. Но поле employee класса Task будет содержать коллекцию сотрудников Employee, которая помечена аннотацией EXTRA. И объекты этой коллекции загружены не будут.
Вариант 2
Принудительно заставляем Hibernate загрузить дочерние объекты для объекта Task.
select task from Task t join fetch t.employee order by t.deadline
С помощью мы явно связываем сущности Task и Employee в нашем запросе. Hibernate об этом и так знает, так как мы используем аннотации @ManyToMany для этих полей.
Но нам нужен оператор join, чтобы дополнить его оператором fetch и получить join fetch. Именно таким способом мы указываем Hibernate, что объекты в коллекциях Task.employee нужно загрузить из базы при выполнении нашего запроса.
Вариант 3
У предыдущего решения есть несколько пробочек. Во-первых, после использования join SQL не вернет нам объекты Task, у которых нет связанных с ними объектов в таблице Employee. Именно так работает inner join.
Поэтому нам нужно дополнить наш join оператором left и превратить его в left join. Пример:
select task from Task t left join fetch t.employee order by t.deadline
Вариант 4
Но и это еще не все. Если в твоем коде отношение между сущностями many-to-may, то возникнут дубликаты в результатах запроса. Один и тот же объект task может быть найден у разных сотрудников (объектов Employee).
Поэтому тебе нужно добавить ключевое слово distinct после слова select, чтобы избавиться от дубликатов объекта Task.
select distinct task from Task t left join fetch t.employee order by t.deadline
Именно так в 4 шага мы пришли к тому запросу, с которого начали. Ну а Java-код будет выглядеть вполне ожидаемо:
String hql = " select distinct task from Task t left join fetch t.employee order by t.deadline";
Query<Task> query = session.createQuery( hql, Task.class);
return query.list();
Ограничения JOIN FETCH
Никто не идеален. Оператор JOIN FETCH тоже. Он имеет достаточно много ограничений. И первое из них — это использование методов setMaxResults() и setFirstResult().
Для оператора JOIN FETCH наш Hibernate сгенерирует очень сложный запрос, в котором объединить в одну три таблицы: employee, task и employee_task. Фактически это запрос не сотрудников или задач, а всех известных пар сотрудник-задача.
И SQL может применить свои операторы LIMIT и OFFSET именно к этому запросу пар сотрудник-задача. В то же время из HQL запроса явно следует, что мы хотим получить именно задачи (Task), и если мы переделим свои параметры FirstResult и MaxResult, то они должны относиться именно к объектам Task.
Если ты напишешь такой код:
String hql = " select distinct task from Task t left join fetch t.employee order by t.deadline";
Query<Task> query = session.createQuery( hql, Task.class);
query.setFirstResult(0);
query.setMaxResults(1);
return query.list();
То Hibernate не сможет правильно преобразовать FirstResult и MaxResult в параметры OFFSET и LIMIT у SQL-запроса.
Вместо этого он сделает три вещи:
- SQL-запрос выберет вообще все данные из таблицы и вернет их Hibernate
- Hibernate сам у себя в памяти отберет нужные записи и вернет их тебе
- Hibernate выдаст предупреждение
Предупреждение будет чем-то типа такого:
WARN [org.hibernate.hql.internal.ast.QueryTranslatorImpl] HHH000104:
firstResult/maxResults specified with collection fetch; applying in memory!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
taskfrom Task t left join fetch t.employeeа надо: select distinct t from Task t left join fetch t.employees тогда и задача пройдет тестирование