Карта / JPA / Hibernate / LazyInitializationException

LazyInitializationException

LazyInitializationException — попытка обратиться к “ленивой” (LAZY) связи после того, как сессия Hibernate уже закрылась. Одна из самых частых ошибок у новичков в JPA.

@Service
class UserService {
    User getUser(Long id) {
        return userRepository.findById(id).get();   // сессия закрывается тут же, после метода
    }
}

// в другом месте:
User user = userService.getUser(1L);
user.getOrders().size();   // 💥 LazyInitializationException — сессия уже закрыта!

orders помечены LAZY (см. тему «FetchType, Cascade, состояния entity») — реальный SQL-запрос за ними выполняется только в момент обращения, а к этому моменту сессия (которая умеет ходить в базу) уже закрыта.

Копнуть глубже

Три способа решить проблему:

  1. Загрузить нужные данные заранее, пока сессия ещё открыта (например, через JOIN FETCH в JPQL):
    @Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id")
    Optional<User> findByIdWithOrders(@Param("id") Long id);
  2. @Transactional на сервисном методе — держит сессию открытой на весь метод, включая обращение к ленивым полям внутри него.
  3. DTO-проекция — сразу выбрать только нужные поля плоским запросом, не работая с ленивыми связями объекта вообще.

Почему просто “сделать всё EAGER” — плохое решение. Это убирает симптом, но не причину: EAGER означает подгрузку всегда, даже когда данные не нужны — это бьёт по производительности на каждом запросе, где загружается объект, независимо от того, нужны ли связанные данные в конкретном случае. Лучше осознанно решать в каждом сценарии, что подгружать.

🎤 Закрыл тему, если можешь объяснить:
• почему возникает LazyInitializationException;
• хотя бы один из трёх способов её избежать и почему "сделать всё EAGER" — плохая идея (если дошёл до 2-го слоя).