«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 Utility
Во-первых, давайте определим класс HibernateUtil для создания SessionFactory с конфигурацией.
Мы будем использовать базу данных HSQLDB в оперативной памяти.
3.2. Entities
Вот наша сущность User :
@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;
}
И связанная сущность Role :
@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;
}
Как мы видим, между пользователем и ролью существует связь «один ко многим».
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. Включение этого параметра означает, что каждый доступ к связанному объекту с отложенной загрузкой будет заключен в новый сеанс, работающий в новой транзакции: наше приложение. Это потому, что мы закончим с проблемой n + 1. Проще говоря, это означает один SELECT для пользователя и N дополнительных SELECT для получения ролей каждого пользователя.
<property name="hibernate.enable_lazy_load_no_trans" value="true"/>
Этот подход неэффективен и также считается анти-шаблоном.
4.3. Использование стратегии FetchType.EAGER
Мы можем использовать эту стратегию вместе с аннотацией @OneToMany, например:
Это своего рода скомпрометированное решение для конкретного использования, когда нам нужно получить связанную коллекцию для большинства наших вариантов использования.
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private Set<Role> roles;
«Таким образом, гораздо проще объявить тип выборки EAGER вместо явного получения коллекции для большинства различных бизнес-потоков.
4.4. Использование выборки соединения
Мы можем использовать директиву JOIN FETCH в JPQL для получения связанной коллекции по запросу, например:
Или мы можем использовать Hibernate Criteria API:
SELECT u FROM User u JOIN FETCH u.roles
Здесь мы указываем связанная коллекция, которая должна быть извлечена из базы данных вместе с объектом User в том же цикле. Использование этого запроса повышает эффективность итерации, поскольку устраняет необходимость извлечения связанных объектов по отдельности.
Criteria criteria = session.createCriteria(User.class);
criteria.setFetchMode("roles", FetchMode.EAGER);
Это наиболее эффективное и детальное решение, позволяющее избежать ошибки LazyInitializationException.
5. Заключение
В этой статье мы увидели, как справиться с org.hibernate.LazyInitializationException : не удалось инициализировать прокси — нет ошибки сеанса.
Мы исследовали различные подходы, а также проблемы с производительностью. Важно использовать простое и эффективное решение, чтобы не влиять на производительность.
Наконец, мы увидели, что подход с выборкой соединения является хорошим способом избежать ошибки.
Как всегда, код доступен на GitHub.
«