Перейти к основному содержимому

Использование аннотации Hibernate @LazyCollection

· 7 мин. чтения

1. Обзор

Управление операторами SQL из наших приложений — одна из самых важных вещей, о которых нам нужно позаботиться, поскольку это оказывает огромное влияние на производительность. При работе с отношениями между объектами есть два основных шаблона проектирования для выборки. Первый — это ленивый подход, а второй — нетерпеливый подход.

В этой статье мы рассмотрим оба из них. Кроме того, мы обсудим аннотацию @LazyCollection в Hibernate.

2. Ленивая выборка

Мы используем ленивую выборку , когда хотим отложить инициализацию данных до тех пор, пока она нам не понадобится . Давайте рассмотрим пример, чтобы лучше понять идею.

Предположим, у нас есть компания, имеющая несколько филиалов в городе. В каждом филиале есть свои сотрудники. С точки зрения базы данных это означает, что у нас есть отношение «один ко многим» между филиалом и его сотрудниками.

При ленивом подходе мы не будем извлекать сотрудников после получения объекта филиала. Мы получаем только данные объекта филиала и откладываем загрузку списка сотрудников до тех пор, пока не вызовем метод getEmployees() . В этот момент будет выполнен еще один запрос к базе данных, чтобы получить сотрудников.

Преимущество этого подхода в том, что мы уменьшаем объем загружаемых данных изначально. Причина в том, что нам могут не понадобиться сотрудники филиала, а загружать их нет смысла, так как мы не планируем использовать их сразу.

3. Нетерпеливый выбор

Мы используем нетерпеливую выборку, когда данные должны быть загружены мгновенно. Давайте возьмем тот же пример компании, филиалов и сотрудников, чтобы объяснить эту идею. Как только мы загрузим какой-либо объект филиала из базы данных, мы сразу же загрузим список его сотрудников, используя тот же запрос к базе данных.

Основная проблема при использовании нетерпеливой выборки заключается в том, что мы загружаем огромное количество данных, которые могут не понадобиться. Следовательно, мы должны использовать его только в том случае, если мы уверены, что данные, которые мы охотно извлекаем, всегда будут использоваться после того, как мы загрузим их объект.

4. Аннотация @LazyCollection

Мы используем аннотацию @LazyCollection , когда нам нужно позаботиться о производительности нашего приложения. Начиная с Hibernate 3.0, @LazyCollection включен по умолчанию. Основная идея использования @LazyCollection заключается в том, чтобы контролировать, следует ли извлекать данные с использованием ленивого или нетерпеливого подхода.

При использовании @LazyCollection у нас есть три параметра конфигурации для параметра LazyCollectionOption : TRUE , FALSE и EXTRA . Давайте обсудим каждый из них отдельно.

4.1. Использование LazyCollectionOption.TRUE

Этот параметр включает ленивую выборку для указанного поля и используется по умолчанию, начиная с Hibernate версии 3.0 . Поэтому нам не нужно явно устанавливать этот параметр. Однако, чтобы лучше объяснить идею, мы возьмем пример, в котором мы установили эту опцию.

В этом примере у нас есть сущность Branch , состоящая из идентификатора , имени и отношения @OneToMany к сущности Employee . Мы можем заметить, что в этом примере мы явно установили для параметра @LazyCollection значение true :

@Entity
public class Branch {

@Id
private Long id;

private String name;

@OneToMany(mappedBy = "branch")
@LazyCollection(LazyCollectionOption.TRUE)
private List<Employee> employees;

// getters and setters
}

Теперь давайте взглянем на сущность Employee , которая состоит из id , name , address , а также отношения @ManyToOne с сущностью Branch :

@Entity
public class Employee {

@Id
private Long id;

private String name;

private String address;

@ManyToOne
@JoinColumn(name = "BRANCH_ID")
private Branch branch;

// getters and setters
}

В приведенном выше примере, когда мы получаем объект филиала, мы не будем сразу загружать список сотрудников . Вместо этого эта операция будет отложена до тех пор, пока мы не вызовем метод getEmployees() .

4.2. Использование LazyCollectionOption.FALSE

Когда мы устанавливаем для этой опции значение FALSE , мы включаем нетерпеливый подход к выборке. В этом случае нам нужно явно указать эту опцию, потому что мы будем переопределять значение Hibernate по умолчанию. Давайте посмотрим на другой пример.

В этом случае у нас есть сущность Branch , которая содержит id , name и отношение @OneToMany с сущностью Employee . Обратите внимание, что мы установили для параметра @LazyCollection значение FALSE :

@Entity
public class Branch {

@Id
private Long id;

private String name;

@OneToMany(mappedBy = "branch")
@LazyCollection(LazyCollectionOption.FALSE)
private List<Employee> employees;

// getters and setters
}

В приведенном выше примере, когда мы получаем объект филиала, мы мгновенно загружаем филиал со списком сотрудников .

4.3. Использование LazyCollectionOption.EXTRA

Иногда нас интересуют только свойства коллекции, и нам не нужны объекты внутри нее сразу.

Например, возвращаясь к примеру с Филиалом и Сотрудником , нам может просто понадобиться количество сотрудников в филиале, не заботясь о реальных сущностях сотрудников. В этом случае мы рассматриваем возможность использования дополнительной опции. Давайте обновим наш пример, чтобы обработать этот случай.

Как и в предыдущем случае, сущность Branch имеет идентификатор , имя и отношение @OneToMany с сущностью Employee . Однако мы устанавливаем параметр @LazyCollection как EXTRA :

@Entity
public class Branch {

@Id
private Long id;

private String name;

@OneToMany(mappedBy = "branch")
@LazyCollection(LazyCollectionOption.EXTRA)
@OrderColumn(name = "order_id")
private List<Employee> employees;

// getters and setters

public Branch addEmployee(Employee employee) {
employees.add(employee);
employee.setBranch(this);
return this;
}
}

Заметим, что в данном случае мы использовали аннотацию @OrderColumn . Причина в том, что опция EXTRA учитывается только для коллекций индексированных списков. Это означает, что если мы не аннотировали поле с помощью @OrderColumn , опция EXTRA даст нам то же поведение, что и lazy, и коллекция будет извлечена при первом доступе.

Кроме того, мы также определяем метод addEmployee() , потому что нам нужно, чтобы Branch и Employee были синхронизированы с обеих сторон. Если мы добавим нового Сотрудника и установим для него филиал, нам также потребуется обновить список сотрудников внутри объекта Филиал .

Теперь, при сохранении одного объекта Branch с тремя связанными сотрудниками, нам нужно будет написать код следующим образом:

entityManager.persist(
new Branch().setId(1L).setName("Branch-1")

.addEmployee(
new Employee()
.setId(1L)
.setName("Employee-1")
.setAddress("Employee-1 address"))

.addEmployee(
new Employee()
.setId(2L)
.setName("Employee-2")
.setAddress("Employee-2 address"))

.addEmployee(
new Employee()
.setId(3L)
.setName("Employee-3")
.setAddress("Employee-3 address"))
);

Если мы посмотрим на выполненные запросы, то заметим, что Hibernate сначала вставит новую ветку для ветки-1. Затем он вставит «Сотрудник-1», «Сотрудник-2», затем «Сотрудник-3».

Мы видим, что это естественное поведение. Однако плохое поведение опции EXTRA заключается в том, что после сброса вышеуказанных запросов будут выполняться три дополнительных — по одному для каждого добавляемого нами сотрудника :

UPDATE EMPLOYEES
SET
order_id = 0
WHERE
id = 1

UPDATE EMPLOYEES
SET
order_id = 1
WHERE
id = 2

UPDATE EMPLOYEES
SET
order_id = 2
WHERE
id = 3

Операторы UPDATE выполняются для установки индекса записи списка . Это пример того, что известно как проблема с запросом N +1 , что означает, что мы выполняем N дополнительных операторов SQL для обновления тех же данных, которые мы создали.

Как мы заметили из нашего примера, у нас может возникнуть проблема с запросом N +1 при использовании опции EXTRA .

С другой стороны, преимущество использования этого варианта заключается в том, что нам нужно получить размер списка сотрудников для каждого филиала:

int employeesCount = branch.getEmployees().size();

Когда мы вызываем этот оператор, он будет выполнять только этот оператор SQL:

SELECT
COUNT(ID)
FROM
EMPLOYEES
WHERE
BRANCH_ID = :ID

Как видим, нам не нужно было хранить список сотрудников в памяти, чтобы получить его размер. Тем не менее, мы советуем избегать опции EXTRA , поскольку она будет выполнять дополнительные запросы.

Здесь также стоит отметить, что можно столкнуться с проблемой запроса N +1 с другими технологиями доступа к данным, поскольку она не ограничивается только JPA и Hibernate.

5. Вывод

В этой статье мы обсудили различные подходы к извлечению свойств объекта из базы данных с помощью Hibernate.

Во-первых, мы обсудили ленивую выборку на примере. Затем мы обновили пример, чтобы использовать нетерпеливую выборку, и обсудили различия.

Наконец, мы показали дополнительный подход к выборке данных и объяснили его преимущества и недостатки.

Как всегда, код, представленный в этой статье, доступен на GitHub .