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 .