Можно ли итерироваться по коллекции, загруженной лениво (Lazy), вне транзакции?

«Можно ли итерироваться по коллекции, загруженной лениво (Lazy), вне транзакции?» — вопрос из категории Hibernate, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Нет, стандартная итерация по лениво загруженной коллекции (FetchType.LAZY) вне управляющей транзакции (или сессии) вызовет исключение LazyInitializationException.

Почему это происходит?

  • Коллекция, помеченная как LAZY, — это прокси-объект, который не содержит реальных данных.
  • При первой попытке доступа (например, вызов size() или iterator()) прокси инициирует запрос к базе данных для загрузки данных.
  • Если на этот момент сессия Hibernate уже закрыта (транзакция завершена), выполнить запрос невозможно, что приводит к исключению.

Пример проблемы:

@Entity
public class Document {
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "document")
    private List<Line> lines; // Ленивая коллекция
}

// В сервисе (транзакция завершена после метода)
@Transactional
public Document getDocument(Long id) {
    return documentRepository.findById(id).orElseThrow();
}

// Позже, вне транзакции (ВЫБРОСИТ LazyInitializationException)
Document doc = service.getDocument(1L);
for (Line line : doc.getLines()) { // Попытка доступа к ленивой коллекции
    System.out.println(line.getText());
}

Правильные решения:

  1. Инициализация внутри транзакции (например, с JOIN FETCH):

    @Query("SELECT d FROM Document d JOIN FETCH d.lines WHERE d.id = :id")
    Document findDocumentWithLines(@Param("id") Long id);
  2. Использование @Transactional на методе, где происходит итерация:

    @Transactional
    public void processDocument(Long id) {
        Document doc = documentRepository.findById(id).orElseThrow();
        for (Line line : doc.getLines()) { // Доступ внутри транзакции
            // ...
        }
    }
  3. Явная инициализация с Hibernate.initialize() (менее предпочтительно):

    @Transactional
    public Document getInitializedDocument(Long id) {
        Document doc = documentRepository.findById(id).orElseThrow();
        Hibernate.initialize(doc.getLines()); // Принудительная загрузка
        return doc; // Коллекция теперь загружена
    }

Вывод: Доступ к ленивым коллекциям должен происходить в контексте открытой сессии Hibernate (транзакции). Планируйте загрузку необходимых данных на этапе выполнения запроса.