1. Обзор
Работая с Hibernate, мы могли столкнуться с ошибкой, которая гласит: org.hibernate.LazyInitializationException: не удалось инициализировать прокси — нет сеанса
.
В этом кратком руководстве мы подробно рассмотрим основную причину ошибки и узнаем, как ее избежать.
2. Понимание ошибки
Доступ к лениво загруженному объекту вне контекста открытого сеанса Hibernate приведет к этому исключению.
Важно понимать , что такое Session , Lazy Initialization и Proxy Object и как они объединяются в среде Hibernate
:
Сеанс
— это контекст сохраняемости, представляющий диалог между приложением и базой данных.Отложенная загрузка
означает, что объект не будет загружен в контекстсеанса до тех пор, пока к нему не будет получен доступ в коде.
- Hibernate создает динамический подкласс
Proxy Object
, который попадет в базу данных только при первом использовании объекта.
Эта ошибка возникает, когда мы пытаемся получить отложенный объект из базы данных с помощью прокси-объекта, но сеанс Hibernate уже закрыт.
3. Пример LazyInitializationException
Давайте посмотрим на исключение в конкретном сценарии.
Мы хотим создать простой объект User
со связанными ролями. Мы будем использовать JUnit для демонстрации ошибки LazyInitializationException
.
3.1. Служебный класс Hibernate
Во-первых, давайте определим класс HibernateUtil
для создания SessionFactory
с конфигурацией.
Мы будем использовать базу данных HSQLDB
в памяти .
3.2. Сущности
Вот наша сущность пользователя :
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@OneToMany
private Set<Role> roles;
}
И связанный объект роли :
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@Column(name = "role_name")
private String roleName;
}
Как мы видим, между User
и Role
существует отношение «один ко многим» .
3.3. Создание пользователя с ролями
Затем мы создадим два объекта Role :
Role admin = new Role("Admin");
Role dba = new Role("DBA");
Мы также создадим пользователя
с ролями:
User user = new User("Bob", "Smith");
user.addRole(admin);
user.addRole(dba);
Наконец, мы можем открыть сеанс и сохранить объекты:
Session session = sessionFactory.openSession();
session.beginTransaction();
user.getRoles().forEach(role -> session.save(role));
session.save(user);
session.getTransaction().commit();
session.close();
3.4. Получение ролей
В этом первом сценарии мы увидим, как правильно получать роли пользователей:
@Test
public void whenAccessUserRolesInsideSession_thenSuccess() {
User detachedUser = createUserWithRoles();
Session session = sessionFactory.openSession();
session.beginTransaction();
User persistentUser = session.find(User.class, detachedUser.getId());
Assert.assertEquals(2, persistentUser.getRoles().size());
session.getTransaction().commit();
session.close();
}
Здесь мы обращаемся к объекту внутри сеанса, поэтому ошибки нет.
3.5. Ошибка получения ролей
Во втором сценарии мы вызовем метод getRoles
вне сеанса:
@Test
public void whenAccessUserRolesOutsideSession_thenThrownException() {
User detachedUser = createUserWithRoles();
Session session = sessionFactory.openSession();
session.beginTransaction();
User persistentUser = session.find(User.class, detachedUser.getId());
session.getTransaction().commit();
session.close();
thrown.expect(LazyInitializationException.class);
System.out.println(persistentUser.getRoles().size());
}
В этом случае мы пытаемся получить доступ к ролям после закрытия сеанса, и в результате код выдает LazyInitializationException
.
4. Как избежать ошибки
Теперь давайте рассмотрим четыре различных решения для устранения ошибки.
4.1. Открыть сеанс на верхнем уровне
Лучшей практикой является открытие сеанса на уровне сохраняемости, например, с помощью шаблона DAO .
Мы можем открыть сеанс на верхних уровнях для безопасного доступа к связанным объектам. Например, мы можем открыть сеанс в слое просмотра .
В результате мы увидим увеличение времени отклика, что скажется на производительности приложения.
Это решение является антипаттерном с точки зрения принципа разделения интересов. Кроме того, это может привести к нарушению целостности данных и длительным транзакциям.
4.2. Включение свойства enable_lazy_load_no_trans
Это свойство Hibernate используется для объявления глобальной политики отложенной загрузки объектов.
По умолчанию это свойство имеет значение false
. Его включение означает, что каждый доступ к связанному объекту с ленивой загрузкой будет заключен в новый сеанс, работающий в новой транзакции:
<property name="hibernate.enable_lazy_load_no_trans" value="true"/>
Не рекомендуется использовать это свойство, чтобы избежать ошибки LazyInitializationException
, так как это снизит производительность нашего приложения. Это потому, что мы закончим с проблемой n + 1 . Проще говоря, это означает один SELECT для пользователя
и N дополнительных SELECT для получения ролей каждого пользователя.
Этот подход неэффективен и также считается анти-шаблоном.
4.3. Использование стратегии FetchType.EAGER
``
Мы можем использовать эту стратегию вместе с аннотацией @OneToMany
:
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private Set<Role> roles;
Это своего рода скомпрометированное решение для конкретного использования, когда нам нужно получить связанную коллекцию для большинства наших случаев использования.
Гораздо проще объявить тип выборки EAGER
вместо того, чтобы явно выбирать коллекцию для большинства различных бизнес-потоков.
4.4. Использование объединенной выборки
Мы также можем использовать директиву JOIN FETCH в
JPQL
для получения связанной коллекции по запросу:
SELECT u FROM User u JOIN FETCH u.roles
Или мы можем использовать Hibernate Criteria API:
Criteria criteria = session.createCriteria(User.class);
criteria.setFetchMode("roles", FetchMode.EAGER);
Здесь мы указываем связанную коллекцию, которая должна быть извлечена из базы данных вместе с объектом User
в том же цикле. Использование этого запроса повышает эффективность итерации, поскольку устраняет необходимость извлечения связанных объектов по отдельности.
Это наиболее эффективное и детальное решение, позволяющее избежать ошибки LazyInitializationException
.
5. Вывод
В этой статье мы узнали, как бороться с org.hibernate.LazyInitializationException: не удалось инициализировать прокси — нет
ошибки сеанса .
Мы исследовали различные подходы, а также проблемы с производительностью. Мы также обсудили важность использования простого и эффективного решения, чтобы не влиять на производительность.
Наконец, мы продемонстрировали, что подход с выборкой соединения является хорошим способом избежать ошибки.
Как всегда, код доступен на GitHub .