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» в реальном сервисе.