Автор оригинала: Petr Shatunov.
1. Обзор
При использовании ленивой загрузки в спящем режиме мы можем столкнуться с исключениями, говоря, что сеанса нет.
В этом уроке мы обсудим, как решить эти проблемы с ленивой загрузкой. Чтобы сделать это, мы будем использовать Spring Boot для изучения примера.
2. Проблемы С Ленивой загрузкой
Цель ленивой загрузки-экономить ресурсы, не загружая связанные объекты в память при загрузке основного объекта. Вместо этого мы откладываем инициализацию ленивых сущностей до того момента, когда они понадобятся. Hibernate использует прокси-серверы и оболочки коллекций для реализации ленивой загрузки.
При извлечении лениво загруженных данных процесс состоит из двух этапов. Во-первых, это заполнение основного объекта, а во-вторых, извлечение данных из его прокси-серверов. Загрузка данных всегда требует открытого сеанса в спящем режиме.
Проблема возникает, когда второй шаг происходит после закрытия транзакции , что приводит к LazyInitializationException .
Рекомендуемый подход заключается в разработке нашего приложения таким образом, чтобы обеспечить получение данных в одной транзакции. Но иногда это может быть трудно при использовании ленивой сущности в другой части кода, которая не может определить, что было загружено или не было загружено.
Hibernate имеет обходной путь, свойство enable_lazy_load_no_trans . Включение этого параметра означает, что каждая выборка ленивой сущности откроет временный сеанс и будет выполняться внутри отдельной транзакции.
3. Пример Ленивой Загрузки
Давайте рассмотрим поведение ленивой загрузки в нескольких сценариях.
3.1 Настройка объектов и служб
Предположим, у нас есть две сущности: Пользователь и Документ . Один Пользователь может иметь много Документов , и мы будем использовать @OneToMany для описания этих отношений. Кроме того, мы будем использовать @Fetch(FetchMode.SUBSELECT) для эффективности.
Следует отметить, что по умолчанию @OneToMany имеет ленивый тип выборки.
Давайте теперь определим нашу сущность User :
@Entity public class User { // other fields are omitted for brevity @OneToMany(mappedBy = "userId") @Fetch(FetchMode.SUBSELECT) private Listdocs = new ArrayList<>(); }
Далее нам нужен слой сервиса с двумя методами, чтобы проиллюстрировать различные варианты. Один из них аннотируется как @Transactional . Здесь оба метода выполняют одну и ту же логику, подсчитывая все документы от всех пользователей:
@Service public class ServiceLayer { @Autowired private UserRepository userRepository; @Transactional(readOnly = true) public long countAllDocsTransactional() { return countAllDocs(); } public long countAllDocsNonTransactional() { return countAllDocs(); } private long countAllDocs() { return userRepository.findAll() .stream() .map(User::getDocs) .mapToLong(Collection::size) .sum(); } }
Теперь давайте подробнее рассмотрим следующие три примера. Мы также будем использовать SQL Statement Count Validator , чтобы понять эффективность решения, подсчитав количество выполненных запросов.
3.2. Ленивая Загрузка С Окружающей Транзакцией
Прежде всего, давайте использовать ленивую загрузку рекомендуемым способом. Итак, мы вызовем наш метод @Transactional на уровне сервиса:
@Test public void whenCallTransactionalMethodWithPropertyOff_thenTestPass() { SQLStatementCountValidator.reset(); long docsCount = serviceLayer.countAllDocsTransactional(); assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount); SQLStatementCountValidator.assertSelectCount(2); }
Как мы видим, это работает и приводит к двум обходам базы данных . Первая поездка туда и обратно выбирает пользователей, а вторая-их документы.
3.3. Ленивая загрузка вне транзакции
Теперь давайте вызовем нетранзакционный метод для имитации ошибки, которую мы получаем без окружающей транзакции:
@Test(expected = LazyInitializationException.class) public void whenCallNonTransactionalMethodWithPropertyOff_thenThrowException() { serviceLayer.countAllDocsNonTransactional(); }
Как и было предсказано, это приводит к ошибке , поскольку функция get Docs пользователя используется вне транзакции.
3.4. Ленивая Загрузка С Автоматической Транзакцией
Чтобы исправить это, мы можем включить это свойство:
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
С включенным свойством мы больше не получаем LazyInitializationException .
Однако подсчет запросов показывает, что в базу данных было совершено шесть обходов туда и обратно . Здесь одна поездка туда и обратно выбирает пользователей, а пять поездок туда и обратно выбирают документы для каждого из пяти пользователей:
@Test public void whenCallNonTransactionalMethodWithPropertyOn_thenGetNplusOne() { SQLStatementCountValidator.reset(); long docsCount = serviceLayer.countAllDocsNonTransactional(); assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount); SQLStatementCountValidator.assertSelectCount(EXPECTED_USERS_COUNT + 1); }
Мы столкнулись с пресловутой проблемой N + 1 , несмотря на то, что мы установили стратегию выборки, чтобы избежать ее!
4. Сравнение подходов
Давайте вкратце обсудим плюсы и минусы.
С включенной недвижимостью нам не нужно беспокоиться о сделках и их границах. Hibernate управляет этим для нас.
Однако решение работает медленно, потому что Hibernate запускает транзакцию для нас при каждой выборке.
Он отлично работает для демо-версий и когда мы не заботимся о проблемах производительности. Это может быть нормально, если используется для извлечения коллекции, содержащей только один элемент, или одного связанного объекта в отношениях один к одному.
Без собственности у нас есть мелкозернистый контроль над транзакциями, и мы больше не сталкиваемся с проблемами производительности.
В целом, это не готовая к производству функция , и документация Hibernate предупреждает нас:
Хотя включение этой конфигурации может привести к тому, что LazyInitializationException исчезнет, лучше использовать план выборки, который гарантирует, что все свойства будут правильно инициализированы до закрытия сеанса.
5. Заключение
В этом уроке мы изучили работу с ленивой загрузкой.
Мы попробовали свойство Hibernate, чтобы помочь преодолеть исключение LazyInitializationException . Мы также видели, как это снижает эффективность и может быть жизнеспособным решением только для ограниченного числа случаев использования.
Как всегда, все примеры кода доступны на GitHub .