HibernateSeniorTechnical

Что такое lazy loading в Hibernate и когда возникает LazyInitializationException?

Lazy loading откладывает загрузку связей до момента обращения к ним. LazyInitializationException возникает, когда сессия уже закрыта, а прокси-объект пытается выполнить SQL-запрос для загрузки данных.

Как работает lazy loading

По умолчанию @OneToMany и @ManyToMany имеют FetchType.LAZY. Hibernate возвращает прокси-объект (PersistentBag/PersistentSet) вместо реальных данных. SQL SELECT для связи выполняется только при первом обращении к коллекции или полю — и только пока Session открыта.

@Entity
public class Department {
    @Id
    private Long id;

    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
    private List<Employee> employees; // прокси, данные не загружены
}

// В рамках открытой сессии — OK
try (Session session = sessionFactory.openSession()) {
    Department dept = session.get(Department.class, 1L);
    int count = dept.getEmployees().size(); // SQL выполнится здесь
}

// После закрытия сессии — LazyInitializationException
Department dept;
try (Session session = sessionFactory.openSession()) {
    dept = session.get(Department.class, 1L);
} // сессия закрыта
dept.getEmployees().size(); // EXCEPTION!

Причины LazyInitializationException

  • Сущность загружена в сервисном слое, возвращена контроллеру — к тому моменту транзакция (и сессия) уже завершена.
  • Использование Open Session in View антипаттерна без его настройки.
  • Сериализация сущности (Jackson) вне транзакции.
  • Детач сущности через session.evict(entity) или EntityManager.detach().

Решение 1: JOIN FETCH в запросе

String hql = "SELECT d FROM Department d JOIN FETCH d.employees WHERE d.id = :id";
Department dept = session
    .createQuery(hql, Department.class)
    .setParameter("id", 1L)
    .uniqueResult();
// employees уже загружены, сессия не нужна

Решение 2: EntityGraph (JPA 2.1+)

@NamedEntityGraph(
    name = "Department.withEmployees",
    attributeNodes = @NamedAttributeNode("employees")
)
@Entity
public class Department { ... }

// Применение
EntityGraph<?> graph = em.getEntityGraph("Department.withEmployees");
Department dept = em.find(Department.class, 1L,
    Map.of("javax.persistence.fetchgraph", graph));

Решение 3: Hibernate.initialize()

// Принудительная загрузка внутри открытой сессии
try (Session session = sessionFactory.openSession()) {
    Department dept = session.get(Department.class, 1L);
    Hibernate.initialize(dept.getEmployees()); // загрузить сейчас
    return dept;
}
// dept.getEmployees() доступен вне сессии

Решение 4: DTO-проекция

String hql = "SELECT new com.example.dto.DeptSummary(d.name, COUNT(e)) "
    + "FROM Department d LEFT JOIN d.employees e GROUP BY d.name";
List<DeptSummary> result = session.createQuery(hql, DeptSummary.class).getResultList();
// DTO — простые объекты, никакого lazy loading

Решение 5: @Transactional охватывает всё чтение

@Service
public class DepartmentService {
    @Transactional(readOnly = true)
    public DepartmentDto getWithEmployees(Long id) {
        Department dept = repo.findById(id).orElseThrow();
        dept.getEmployees().size(); // сессия открыта, OK
        return mapper.toDto(dept);
    }
}

Подводные камни

  • Open Session in View: в Spring Boot включён по умолчанию (spring.jpa.open-in-view=true) — скрывает проблему, но выполняет SQL прямо в слое представления. Отключайте его и явно загружайте нужные данные.
  • EAGER не панацея: FetchType.EAGER на @OneToMany вызывает N+1 при любом запросе коллекции сущностей.
  • JOIN FETCH + пагинация: Hibernate предупреждает HHH90003004 и делает пагинацию в памяти — сначала загружает всё, потом обрезает.
  • Несколько JOIN FETCH коллекций: нельзя делать JOIN FETCH двух bag-коллекций одновременно (MultipleBagFetchException) — используйте Set или загружайте в два запроса.
  • Детач через serialization: Jackson пытается сериализовать все поля, включая lazy-прокси. Используйте @JsonIgnore или DTO.
  • session.load() vs session.get(): load() всегда возвращает прокси (даже для скалярных значений) — LazyInitializationException может прийти неожиданно даже без коллекций.
  • Транзакционный прокси и self-invocation: вызов @Transactional-метода из того же бина не создаёт транзакцию — сессия не открывается, lazy loading падает.

Common mistakes

  • Путать термин «lazy loading lazyinitializationexception» с соседним механизмом Hibernate.
  • Не называть границу lifecycle, transaction, thread или request для «lazy loading lazyinitializationexception».
  • Игнорировать production-эффекты «lazy loading lazyinitializationexception»: latency, SQL shape, memory, security или observability.

What the interviewer is testing

  • Попросить объяснить механизм «lazy loading lazyinitializationexception» на минимальном примере.
  • Проверить, видит ли кандидат failure mode и диагностику для «lazy loading lazyinitializationexception».
  • Уточнить, какие настройки или API меняют «lazy loading lazyinitializationexception» в реальном сервисе.

Sources

Related topics