LazyCollectionOption.EXTRA

Модуль 4. Работа с БД
14 уровень , 2 лекция
Открыта

Знакомимся с LazyCollectionOption.EXTRA

Но самый большой интерес представляет значение LazyCollectionOption.EXTRA. Если ты указываешь его как значение аннотации @LazyCollection, то Hibernate будет оттягивать загрузку элементов коллекции как можно дольше.

Если ты попробуешь получить количество элементов коллекции:


User user = session.load(User.class, 1);             	
List<Comment> comments = user.getComments();
int count = commetns.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 = commetns.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;
}

Главное преимущество 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 очень сильно ускоряет работу с базой.

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 запрос в базу, хотя мог бы обойтись и одним. Если бы заранее знал, что тебе понадобятся все объекты данного класса.

1
Задача
Модуль 4. Работа с БД, 14 уровень, 2 лекция
Недоступна
task1403
Есть два класса-энтити: Employee и StatisticView. У Employee замени параметр fetch аннотации...
Комментарии (13)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Антон Уровень 115
19 февраля 2025
Основная идея вроде верная, но объяснение путаное. В связях @OneToMany и @ManyToMany по умолчанию используется FetchType.LAZY, а значит, коллекции загружаются только при первом обращении. Однако к этому относятся не только обычные вызовы типа getCollection(), но и такие методы, как size(), add(), remove(). И в этом, собственно, и состоит проблема. Зачем нам загружать всю коллекцию, когда мы просто добавляем новый элемент add(), удаляем старый remove(), или просто узнаем размер коллекции size()? Эту проблему как раз и решает LazyCollectionOption.EXTRA. При ее использовании Hibernate изменяет поведение size(), add(), remove(): • size() выполняет обычный SELECT COUNT(*) без загрузки всей коллекции. • add() сразу выполняет INSERT, без SELECT *. • remove() удаляет запись через DELETE, без загрузки коллекции. В противном случае, если LazyCollectionOption.EXTRA не указана, то при простом добавлении add() у нас будет загружаться вся коллекция.
Евгений Уровень 97
7 февраля 2025

task.employees.add(director);
Каким образом это должно работать???? Никого это не смутило?
Антон Уровень 115
19 февраля 2025
А что не так? Имеешь в виду, что поле employees объявлено как private и к нему нельзя напрямую обращаться?
Михаил Шапошников Уровень 1 Expert
18 апреля 2024
На сколько я понимаю , описание тут проблемы N+1 не верно. Когда у нас есть один пользователь (User) с 1000 комментариями (Comment), и если связь между User и Comment настроена как ленивая загрузка (FetchType.LAZY), то при первом обращении к списку комментариев будет выполнен один дополнительный запрос для их получения. Итого получается два запроса: один для получения пользователя и один для получения всех его комментариев. Однако, если у нас есть запрос, который извлекает 10 пользователей, и для каждого пользователя необходимо загрузить его комментарии, то без соответствующей оптимизации (например, использования JOIN FETCH в JPQL или EntityGraph в JPA) Hibernate выполнит 1 запрос для получения всех пользователей и 10 отдельных запросов для получения комментариев каждого пользователя, в сумме 11 запросов.
fedyaka Уровень 36
15 мая 2024
В любом случае получается одно и то же N+1
Ольга Николенко Уровень 109 Expert
7 июня 2024
возможно что-то в лекции изменилось, но вы говорите о режиме загрузки FetchType.LAZY по параметру Fetch, при котором происходит ленивая, но полная загрузка коллекций а в проблеме N+1 описана аннотация LazyCollection с значением LazyCollectionOption.EXTRA, при которой не происходит полной загрузки коллекции, загрузка каждого элемента коллекции происходит уже в цикле
Max Dudin Уровень 6 Expert
13 октября 2023
а вот он и @OrderColumn всплыл, из задачи решенной пару тройку лекций назад
fedyaka Уровень 36
8 января 2023
есть вопрос а в спринг дата при(где используется hibernate), то при lazy загрузке списка, если мы просто добавляем эллемент в список и сохраняем, то будет ли вытащен весь список а после уже добавлен эллемент или просто сохраниться?
Дмитрий Уровень 37
17 марта 2024
исходя из лекции, если не использовать LazyCollectionOption.EXTRA, то будет дернут весь список. По правьте, если ошибаюсь
Владимир Уровень 109 Expert
7 января 2023
лично меня так сбивает с толку то что класс EmployeeTask ссылается на таблицу task, а ведь есть же еще служебная таблица employee_task, которая связывает сущности Employee и EmployeeTask.
fedyaka Уровень 36
8 января 2023
а ещё там каскадное удаление записей по обе стороны, поэтому если удалят хоть одну запись, то по связям между пользователями и тасками, удалиться вся база данных нахрен(но это не точно, может останется парочка) :))
Anonymous #2957882 Уровень 1
16 января 2023
Это упрощенная версия реальных ситуаций, особенно с точки зрения аналитика) Обычно есть основная таблица с некими заявками, 2-3 таблицы с атрибутами, и к каждой справочник, потому что атрибуты это бывает 1-2 буквы или цифры. Когда-то видел, что к 1 таблице порядка 40 таблиц атрибутов и справочников, я даже не понял как с этим разбираться)
Павел Уровень 111 Expert
10 декабря 2023
Да, в приведённом примере класс EmployeeTask следовало бы назвать Task, иначе запутано.