Рубрики
Без рубрики

A (окончательный?) руководство по исключению ленивой инициализации

Сообщения, которые были написаны об исключении Hibernate LazyInitializationException, вероятно, могли бы заполнить все… Помечен как java, гибернация, jpa, постоянство.

Сообщения, которые были написаны о Hibernate LazyInitializationException , вероятно, могли бы заполнить целые книги. Тем не менее, я считаю, что каждый из них фокусируется на определенном аспекте этого: некоторые о конкретном решении, некоторые о том, как решить эту проблему с помощью Spring Boot и т. Д. Я бы хотел, чтобы этот пост стал окончательным руководством по этому вопросу, хотя я почти уверен, что этого не произойдет. По крайней мере, я смогу указать на это другим.

Первопричина

Любите ли вы или ненавидите ORM фреймворки в целом, они, тем не менее, довольно распространены в экосистеме Java. |/JPA является стандартом ORM и частью спецификаций Jakarta EE. Спящий режим является его наиболее распространенной реализацией: например, он используется по умолчанию в Spring Boot.

Давайте сначала опишем причину исключения.

Спецификации JPA занимают 568 страниц, поэтому позвольте мне воспользоваться некоторыми сокращениями. Пожалуйста, при необходимости обратитесь к источнику .

JPA определяет сущности и отношения :

  • Сущность довольно проста. Он представляет собой таблицу базы данных. Например, объект Клиент сопоставляется с таблицей КЛИЕНТ .
  • Связь соединяет одну сущность с другой. Это может быть один-к-одному, один-ко-многим, много-к-одному и много-ко-многим. Например, объект Клиент имеет отношение “один к одному” с объектом Корзина и отношение “один ко многим” с объектом Заказ .

Отношения могут быть нетерпеливыми или ленивыми .

Hibernate извлекает данные в нетерпеливых отношениях в одном запросе. По умолчанию поддерживаются отношения “один к одному”. В приведенном выше примере это означает, что загрузка Клиента также загружает его Тележка . Это em.найти(Customer.class , 1L) генерирует следующий SQL:

SELECT customer0_.id as id1_2_0_, customer0_.cart_id AS cart_id2_2_0_, cart1_.id as id1_1_1_
FROM Customer customer0_
    LEFT OUTER JOIN Cart cart1_ ON customer0_.cart_id = cart1_.id
WHERE customer0_.id = 1L

С другой стороны, отношения “один ко многим” по умолчанию являются ленивыми. Для таких отношений режим гибернации не извлекает данные во время запроса. Вместо этого он инициализирует атрибут, который ссылается на отложенную связь с прокси-сервером . Этот прокси-сервер содержит ссылку на сеанс гибернации , который загрузил корневой объект. Hibernate выполняет новый запрос, когда вы обращаетесь к атрибуту с помощью указанного Сеанса (JPA EntityManager завершает Сеанс ).

Это имеет два важных последствия:

  1. Это негативно сказывается на производительности, так как вам требуется дополнительный доступ к базе данных в оба конца.
  2. Если Сеанс закрыт – или объект отсоединен, Hibernate не может подключиться к базе данных и выдает страшное исключение LazyInitializationException !

Давайте посмотрим, как это работает:

var manager = factory.getEntityManager();                         // 1
var transaction = manager.getTransaction();
transaction.begin();                                              // 2
var newCustomer = new Customer();
newCustomer.addOrder(new Order());
manager.persist(newCustomer);                                     // 3
transaction.commit();                                             // 4
var id = newCustomer.getId();
var anotherManager = factory.getEntityManager();                  // 5
var customer = anotherManager.find(Customer.class, id);           // 6
anotherManager.detach(customer);                                  // 7
var orders = customer.getOrders();                                // 8
assertThrows(LazyInitializationException.class, orders::isEmpty); // 9
  1. Получите JPA EntityManager при условии, что фабрика является EntityManagerFactory .
  2. Начните транзакцию.
  3. Сохраняйте вновь созданный объект в базе данных.
  4. Зафиксируйте транзакцию.
  5. Получить другой EntityManager . Это важно, потому что Hibernate кэширует сущность в объекте Session (см. [Гибернация Гидратной Вики]’]( https://github.com/arey/hibernate-hydrate ) ).
  6. Загрузите ранее сохраненный Клиент под новой ссылкой.
  7. Удалите Клиента из EntityManager . Если мы этого не сделаем, Hibernate выполнит новый запрос с использованием EntityManager .
  8. Получите ссылку на атрибут заказы .
  9. Любой вызов метода orders вызывает исключение, потому что это не “настоящая” коллекция, а прокси-сервер.

Приведенный выше образец выглядит довольно запутанным, чтобы воспроизвести проблему. Однако в веб-приложениях это просто, так как каждый новый запрос должен порождать новый EntityManager .

Возможные решения

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

Страстные ассоциации

Я упоминал выше, что ассоциации “один ко многим” по умолчанию ленивы, а “один к одному” – нетерпеливы. JPA позволяет вам изменять это значение по умолчанию с помощью аннотаций.

@Entity
public class Customer {

    @OneToOne(cascade = CascadeType.ALL)
    private Cart cart;

    @OneToMany(fetch = FetchType.EAGER)  // 1
    private Set orders;
}
  1. Запрашивайте набор заказов с нетерпением при загрузке Клиент экземпляр

Ниже приведен новый сгенерированный запрос:

SELECT customer0_.id        AS id1_2_0_,
       customer0_.cart_id   AS cart_id2_2_0_,
       cart1_.id            AS id1_1_1_,
       orders2_.CUSTOMER_ID AS customer2_0_2_,
       orders2_.id          AS id1_0_2_,
       orders2_.id          AS id1_0_3_
FROM Customer customer0_
         LEFT OUTER JOIN Cart cart1_      ON customer0_.cart_id = cart1_.id
         LEFT OUTER JOIN "ORDER" orders2_ ON customer0_.id = orders2_.CUSTOMER_ID
WHERE customer0_.id = ?

Настраивать свои ассоциации на нетерпеливость – ужасная идея! Вот три веские причины, по которым ассоциации “один ко многим” по умолчанию ленивы – и должны оставаться такими:

  1. Нетерпеливо загруженный объект может сам нетерпеливо загружать другие объекты. Это черепаха на всем пути вниз.
  2. Как только вы настроили ассоциацию на готовность, ее невозможно настроить. Данные будут загружены независимо от того, нужны они вам в дальнейшем или нет.
  3. Коллекции не имеют ограничений по размеру.

Соединяя их вместе, мы получаем граф объектов с:

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

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

Открытая Сессия На Виду

Я упоминал выше, что JPA часто используется в веб-приложениях. В некоторых контекстах разработчики используют сущности непосредственно в представлениях например |/JSP . В традиционных многоуровневых архитектурах EntityManager открывается - и закрывается - на уровне сервиса. Использование лениво загруженного атрибута в представлении вызывает исключение LazyInitializationException .

Идея дизайна OSIV состоит в том, чтобы создать новый EntityManager и использовать его, когда это произойдет. Это реализовано с помощью Фильтра . Хорошая вещь в OSIV заключается в том, что она работает.

Плохо то, что каждый лениво загруженный атрибут на графике выполняет новый SQL-запрос и требует нового обращения к базе данных. Это известно как проблема N + 1 : один запрос для извлечения корневой сущности и N запросов для извлечения каждой дочерней сущности. Помните график объектов из раздела выше? Количество обходов зависит от уровня вложенности.

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

Имейте в виду, что OSIV очень распространен в экосистеме Java. Это описано в первом издании Java Persistence с гибернацией. Вы можете найти его в Интернете в нескольких местах. Даже Spring Boot регистрирует фильтр OSIV по умолчанию, хотя он регистрирует предупреждающее сообщение об этом. Чтобы избежать неприятных сюрпризов в процессе производства, я бы посоветовал вам установить spring.jpa.open-in-view=false в качестве первого шага в каждом проекте Spring Boot, который использует как Hibernate, так и Web MVC.

Объекты Передачи Данных

DTO были довольно популярны в многоуровневых архитектурах 10 лет назад. Идея DTO состоит в том, чтобы создать выделенный тип (вместо типа сущности) для отправки в представление. Чтобы создать экземпляр DTO, вам необходимо прочитать соответствующие атрибуты сущности и записать их значения в TO.

Однако в большинстве приложений тип DTO тесно связан – если не один к одному – с типом сущности. Копирование и вставка каждого атрибута вручную – это скучно и чревато ошибками. Чтобы улучшить это, появилось совершенно новое поколение фреймворков для сопоставления между компонентами.

Преимущество DTO заключается в том, что, поскольку сопоставление происходит на уровне сервиса, вы можете соответствующим образом обработать ошибку. Однако это не решает проблему N + 1.

Дешевый трюк состоит в том, чтобы избежать, сохранить сущность и явно вызвать ее получатели перед отправкой в представление. У него те же плюсы и минусы, что и у DTO, соответственно, обработка ошибок в нужном месте и проблема с запросами N + 1.

Впадать в спячку Увлажнять

Я наткнулся на Гидрат Гибернации, исследуя этот пост.

Как упоминалось в javadoc Hibernate, исключение LazyInitializationException указывает на доступ к необработанным данным вне контекста сеанса гибернации. Например, когда доступ к неинициализированному прокси-серверу или коллекции осуществляется после закрытия сеанса или после отсоединения объекта от сеанса.

Чтобы решить эту проблему, вы можете предварительно выбрать все необходимые свойства до закрытия сеанса или использовать шаблон OpenSessionInView. В первом случае вы можете вручную вызвать соответствующий механизм получения полей или использовать ключевое слово fetch join в запросах JPQL или HQL.

Проект Hibernate hydrate помогает разработчику автоматически разрешать неинициализированные прокси-серверы. Один вспомогательный класс или 2, если вы используете полный JPA с гибернацией в качестве поставщика.

Гибернация Гидрата Вики

Проект предоставляет несколько методов глубокого гидрата() , которые требуют двух параметров:

  • Переход в спящий режим Сеанс или Фабрика сеансов . Если вы используете интерфейс JPA EntityManager , легко получить базовую сессию легко, позвонив manager.развернуть(Session.class )
  • Сущность или совокупность сущностей для “гидратации”.

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

Принести присоединиться

Все вышеперечисленные решения имеют проблемы, связанные с производительностью:

  • НЕТЕРПЕЛИВЫЙ загружает весь график объектов в память, независимо от того, необходимо это или нет.
  • У других альтернатив действительно есть проблема с N + 1 запросами. Гибернация гидрата даже отображает обе проблемы.

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

Но я считаю, что это неправильный взгляд на это. Мы знаем, какие атрибуты нам нужны в представлении, чтобы мы могли быстро их загрузить. Проблема в том, что эти атрибуты, вероятно, различны для каждого представления: нам нужен способ настроить характеристику рвения/лени для каждой ассоциации по требованию. Это цель fetch присоединиться .

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

Они доступны:

  • В JPQL :

  • В API критериев:

Запустив один из приведенных выше фрагментов кода, Hibernate извлекает атрибут заказы экземпляра Клиента в том же запросе, который загружает сам Клиент . Вот соответствующий SQL-запрос:

SELECT customer0_.id        AS id1_2_0_,
       orders1_.id          AS id1_0_1_,
       customer0_.cart_id   AS cart_id2_2_0_,
       orders1_.CUSTOMER_ID AS customer2_0_0__,
       orders1_.id          AS id1_0_0__
FROM Customer customer0_
         LEFT OUTER JOIN "ORDER" orders1_ ON customer0_.id = orders1_.CUSTOMER_ID
WHERE customer0_.id = 1

Граф сущностей

Соединения с выборкой выполняют свою работу: они охотно загружают ленивые атрибуты для запроса.

Тем не менее, они страдают от двух недостатков:

  • Использование соединений выборки подвержено ошибкам, особенно в JPQL.
  • В большинстве случаев для представлений обычно требуется один и тот же набор атрибутов. Тем не менее, соединения выборки требуют, чтобы вы устанавливали их в каждом необходимом запросе.

По этой причине в JPA 2.1 используется концепция графа сущностей . Граф сущностей аналогичен многоразовому декларативному соединению выборки по набору атрибутов. Как следует из названия, вы можете применить граф сущностей ко всему подграфу – он не ограничивается плоскими атрибутами.

Граф сущностей можно определить как декларативно с помощью аннотаций, так и программно:

@Entity
@NamedEntityGraph(
    name = "orders",
    attributeNodes = { @NamedAttributeNode("orders") }
)
public class Customer {

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "CUSTOMER_ID")
    private Set orders;
}

Вы можете использовать график сущностей как через JPQL, так и через API критериев. Что еще более важно, вы можете использовать его в простых методах find() .

var entityGraph = em.getEntityGraph("orders");
var props = Map.of("javax.persistence.loadgraph", entityGraph);
var customer = anotherManager.find(Customer.class, id, props);

Вывод

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

С JPA 2.1+ у нас есть возможность (и я считаю, что мы должны!) используйте графики сущностей. До этого мы должны вернуться, чтобы получить соединения. Если этого не произойдет, вы, по крайней мере, будете знать, с какими проблемами столкнетесь.

Идти дальше:

Первоначально опубликовано на Фанат Java 28 марта th 2021

Оригинал: “https://dev.to/nfrankel/a-definitive-guide-on-lazyinitializationexception-1l4a”