1. Опис проблеми
Як ми вже сказали вище, анотація 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();
2. Обмеження 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!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ