1. Знайомимося з LazyCollectionOption.EXTRA

Але найбільший інтерес являє собою значення LazyCollectionOption.EXTRA. Якщо ти вказуєш його як значення анотації @LazyCollection, то Hibernate відтягуватиме завантаження елементів колекції якомога довше.

Якщо ти спробуєш отримати кількість елементів колекції:


User user = session.load(User.class, 1);
List<Comment> comments = user.getComments();
int count = comments.size();

То для всього цього коду Hibernate виконає лише один запит:

SELECT COUNT(id) FROM comment WHERE user_id=1;

Але якщо ти хочеш отримати один коментар із колекції, наприклад, номер 3:


User user = session.load(User.class, 1);
List<Comment> comments = user.getComments();
Comment comment = comments.get(3);

То виникне питання: а як Hibernate повинен дізнатися, що елемент саме третій, не завантажуючи на згадку всі елементи?

Для вирішення цієї проблеми пропонується зробити в таблиці comment додаткову колонку, яка зберігатиме порядковий номер коментаря в колекції коментарів. Також для цього потрібна спеціальна анотація — @OrderColumn.

Ось як виглядатиме це рішення:


@Entity
@Table(name="user")
class User {
   @Column(name=”id”)
   public Integer id;
 
   @OneToMany(cascade = CascadeType.ALL)
   @LazyCollection(LazyCollectionOption.EXTRA)
   @OrderColumn(name = "order_id")
   public List<Comment> comments;
}

2. Головна перевага LazyCollectionOption.EXTRA

Найбільшу перевагу LazyCollectionOption.EXTRA ми можемо спостерігати, коли вказуємо її в анотації @ManyToMany. Візьмемо наш старий випадок, коли ми маємо Employee, Task, і можна призначати багато завдань на одного користувача.

Наші Java-класи виглядають так:

Клас Employee:


@Entity
@Table(name=”employee”)
class Employee {
   @Column(name=”id”)
   public Integer id;
 
   @ManyToMany(cascade = CascadeType.ALL)
   @JoinTable(name="employee_task",
       joinColumns= @JoinColumn(name="employee_id", referencedColumnName="id"),
       inverseJoinColumns= @JoinColumn(name="task_id", referencedColumnName="id") )
   @LazyCollection(LazyCollectionOption.EXTRA)
   private Set<EmployeeTask> tasks = new HashSet<EmployeeTask>();
 
}

І клас EmployeeTask:


@Entity
@Table(name=”task”)
class EmployeeTask {
   @Column(name=”id”)
   public Integer id;
 
   @ManyToMany(cascade = CascadeType.ALL)
   @JoinTable(name="employee_task",
       joinColumns= @JoinColumn(name="task_id", referencedColumnName="id"),
       inverseJoinColumns= @JoinColumn(name="employee_id", referencedColumnName="id") )
   @LazyCollection(LazyCollectionOption.EXTRA)
   private Set<Employee> employees = new HashSet<Employee>();
 
}

А щоб додати завдання директорові, потрібно написати приблизно такий код:


Employee director = session.find(Employee.class, 4);
EmployeeTask task = session.find(EmployeeTask.class, 101);
task.employees.add(director);
 
session.update(task);
session.flush();

Так ось, якщо поле employees у класі Task має анотацію LazyCollectionOption.EXTRA, колекції employees (у класу Task) та колекції task (у класу Employee) взагалі не будуть завантажені з бази жодного разу.

Під час виконання цього коду буде вставлено лише один запис до службової таблиці employee_task, яка, як ти пам'ятаєш, виглядає приблизно так:

Таблиця employee_task:
employee_id task_id
1 1
2 2
5 3
5 4
5 5
4 7
6 8
4 101

Зеленим кольором виділено доданий рядок. Щоб додати цей рядок, не потрібно завантажувати колекції з бази — Hibernate обійдеться без цього. Це саме той випадок, коли LazyCollectionOption.EXTRA дуже прискорює роботу з базою.

3. N+1 Problem

Але, звісно, має цей режим і зворотний бік. Так, анотація LazyCollectionOption.EXTRA породжує проблему N+1 Problem.

Якщо ти вирішиш пройтися всіма коментарями свого користувача:


User user = session.load(User.class, 1);
List<Comment> comments = user.getComments();
for (Comment comment : comments) {
    System.out.println(comment);
}

То Hibernate буде виконувати за окремим запитом для кожного об'єкта Comment. А також ще один додатковий запит, щоб отримати кількість усіх коментарів. Це може суттєво уповільнити роботу коду.

Якщо у твого користувача 1000 коментарів, то для виконання цього коду Hibernate зробить 1001 запит до бази, хоча міг би обійтися й одним. Якби знав, що тобі знадобляться всі об'єкти цього класу.