Перейти к основному содержимому

Краткое руководство по свойству Hibernate enable_lazy_load_no_trans

· 4 мин. чтения

1. Обзор

При использовании отложенной загрузки в Hibernate мы можем столкнуться с исключениями, говорящими об отсутствии сеанса.

В этом уроке мы обсудим, как решить эти проблемы с ленивой загрузкой. Для этого мы будем использовать Spring Boot для изучения примера.

2. Проблемы с ленивой загрузкой

Цель ленивой загрузки — сэкономить ресурсы, не загружая связанные объекты в память при загрузке основного объекта. Вместо этого мы откладываем инициализацию ленивых сущностей до момента, когда они понадобятся. Hibernate использует прокси-серверы и обертки коллекций для реализации ленивой загрузки.

Процесс получения лениво загруженных данных состоит из двух этапов. Во-первых, это заполнение основного объекта, а во-вторых, извлечение данных из его прокси. Для загрузки данных всегда требуется открытая сессия в Hibernate.

Проблема возникает, когда второй шаг происходит после закрытия транзакции , что приводит к исключению LazyInitializationException .

Рекомендуемый подход заключается в разработке нашего приложения таким образом, чтобы поиск данных происходил за одну транзакцию. Но иногда это может быть сложно при использовании ленивого объекта в другой части кода, который не может определить, что было загружено, а что нет.

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

3. Пример ленивой загрузки

Давайте посмотрим на поведение отложенной загрузки в нескольких сценариях.

3.1. Настройка сущностей и служб

Предположим, у нас есть две сущности: User и Document . У одного пользователя может быть много Document , и мы будем использовать @OneToMany для описания этих отношений. Кроме того, мы будем использовать @Fetch(FetchMode.SUBSELECT) для повышения эффективности.

Следует отметить, что по умолчанию @OneToMany имеет тип отложенной выборки.

Давайте теперь определим нашу сущность User :

@Entity
public class User {

// other fields are omitted for brevity

@OneToMany(mappedBy = "userId")
@Fetch(FetchMode.SUBSELECT)
private List<Document> docs = 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();
}
}

Теперь давайте подробнее рассмотрим следующие три примера. Мы также будем использовать SQLStatementCountValidator , чтобы понять эффективность решения, подсчитав количество выполненных запросов.

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();
}

Как и предполагалось, это приводит к ошибке , поскольку функция getDocs пользователя используется вне транзакции.

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 .