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

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

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

Автор оригинала: 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 List 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();
    }
}

Теперь давайте подробнее рассмотрим следующие три примера. Мы также будем использовать 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 .