«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.

«