Опис проблеми

Як ми вже сказали вище, анотація 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!