Hibernate не вдалося ініціалізувати проксі - немає сеансу

1. Огляд

Працюючи з Hibernate, ми могли зіткнутися з помилкою: org.hibernate.LazyInitializationException: не вдалося ініціалізувати проксі - немає сеансу .

У цьому короткому посібнику ми детальніше розглянемо першопричину помилки та дізнаємося, як її уникнути.

2 Розуміння помилки

Доступ до завантаженого ледачим об’єктом поза контекстом відкритого сеансу сплячого режиму призведе до цього винятку.

Важливо зрозуміти, що таке сеанс , лінива ініціалізація та об’єкт проксі, і як вони об’єднуються в рамках сплячого режиму .

  • Сеанс - це контекст стійкості, який представляє розмову між програмою та базою даних
  • Ледаче завантаження означає, що об’єкт не буде завантажений до контексту сеансу, поки до нього не буде здійснено доступ у коді.
  • Hibernate створює динамічний підклас об’єкта проксі, який потрапляє в базу даних лише тоді, коли ми вперше використовуємо об’єкт.

Ця помилка означає, що ми намагаємось отримати ледаче завантажений об'єкт із бази даних за допомогою об'єкта-проксі, але сеанс сплячого режиму вже закритий.

3. Приклад для LazyInitializationException

Побачимо виняток у конкретному сценарії.

Ми хочемо створити простий об'єкт User із пов'язаними ролями. Давайте використаємо JUnit для демонстрації помилки LazyInitializationException .

3.1. Клас утилізації сплячого режиму

Спочатку визначимо клас 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 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; }

Як бачимо, між Користувачем та Роллю існує взаємозв'язок "один до багатьох" .

3.3. Створення користувача з ролями

Далі, давайте створимо два об’єкти ролі :

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 використовується для декларування глобальної політики для завантаження об’єктів, що завантажуються ліниво.

За замовчуванням ця властивість хибна . Увімкнення означає, що кожен доступ до асоційованої ледаче завантаженої сутності буде перенесено в новий сеанс, що працює в новій транзакції:

Using this property to avoid LazyInitializationException error is not recommended since it will slow down the performance of our application. This is because we'll end up with an n + 1 problem. Simply put, that means one SELECT for the User and N additional SELECTs to fetch the roles of each user.

This approach is not efficient and also considered an anti-pattern.

4.3. Using FetchType.EAGER Strategy

We can use this strategy along with a @OneToMany annotation, for example :

@OneToMany(fetch = FetchType.EAGER) @JoinColumn(name = "user_id") private Set roles;

This is a kind of compromised solution for a particular usage when we need to fetch the associated collection for most of our use cases.

So it's much easier to declare the EAGER fetch type instead of explicitly fetching the collection for most of the different business flows.

4.4. Using Join Fetching

We can use a JOIN FETCH directive in JPQL to fetch the associated collection on-demand, for example :

SELECT u FROM User u JOIN FETCH u.roles

Or we can use the Hibernate Criteria API :

Criteria criteria = session.createCriteria(User.class); criteria.setFetchMode("roles", FetchMode.EAGER);

Here, we specify the associated collection that should be fetched from the database along with the User object on the same round trip. Using this query improves the efficiency of iteration since it eliminates the need for retrieving the associated objects separately.

This is the most efficient and fine-grained solution to avoid the LazyInitializationException error.

5. Conclusion

У цій статті ми побачили, як боротися з org.hibernate.LazyInitializationException: не вдалося ініціалізувати проксі - помилка сеансу відсутня .

Ми досліджували різні підходи разом із проблемами продуктивності. Важливо використовувати просте та ефективне рішення, щоб уникнути впливу на продуктивність.

Нарешті, ми побачили, як підхід до об’єднання є хорошим способом уникнути помилки.

Як завжди, код доступний на GitHub.