Ответ
LazyInitializationException — это исключение времени выполнения в Hibernate, которое возникает при попытке доступа к лениво загружаемой (LAZY) коллекции или прокси-объекту вне активной сессии (Session) или транзакции.
Причина
Hibernate использует прокси-объекты для ленивых ассоциаций (например, @OneToMany(fetch = FetchType.LAZY)). Когда сессия закрыта, прокси не может выполнить SQL-запрос для подгрузки реальных данных, что и вызывает исключение.
Типичный пример:
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items; // Ленивая коллекция
// getters/setters
}
// В сервисном слое или контроллере:
Order order = session.get(Order.class, orderId);
session.close(); // Сессия закрывается здесь (например, после @Transactional)
// Попытка доступа к ленивой коллекции ВНЕ сессии:
order.getItems().size(); // LazyInitializationException!
Способы решения
-
Инициализация в рамках сессии: Используйте
Hibernate.initialize()до закрытия сессии.Order order = session.get(Order.class, orderId); Hibernate.initialize(order.getItems()); // Явная инициализация session.close(); order.getItems().size(); // OK -
Использование
JOIN FETCHв JPQL/HQL: Загрузите связанные данные одним запросом.String jpql = "SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id"; Order order = entityManager.createQuery(jpql, Order.class) .setParameter("id", orderId) .getSingleResult(); -
Паттерн "Open Session in View" (OSIV): Сессия остается открытой на время всего HTTP-запроса (до рендеринга представления). Внимание: Этот подход может приводить к долгим соединениям с БД и считается антипаттерном в некоторых контекстах.
-
DTO-проекции или
@EntityGraph: Загружайте только необходимые данные, используя проекции запросов или аннотацию@EntityGraphв Spring Data JPA.
Важно: Изменение стратегии загрузки на EAGER (fetch = FetchType.EAGER) обычно не рекомендуется, так как это может привести к проблемам производительности (N+1 query) и загрузке ненужных данных.
Ответ 18+ 🔞
А, ну вот, смотри, опять про эти ваши параллельные стримы заговорили. Ну, типа, parallelStream() в Java — это такая штука, которая пытается изобразить из себя супермена и обрабатывать твою коллекцию не в одном потоке, а сразу в нескольких, чтобы быстрее было. В теории, конечно.
Представь, у тебя есть список чисел, как мешок картошки. Обычный stream() — это ты один, горбатишься, чистишь каждую картофелину по очереди. А parallelStream() — это ты позвал соседей-алкашей, раздал им ножи и кричишь: «Ребята, давайте все вместе, быстренько!». Вроде как должно быть веселее и шустрее.
List<Integer> chisla = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Ты один, грустный, перемножаешь всё подряд
long poshlepanSum = chisla.stream()
.mapToLong(i -> i * 2)
.sum();
// А тут — овердохуища потоков! (ну, в теории)
long parallelSum = chisla.parallelStream() // Включаем режим "раздербанивания"
.mapToLong(i -> i * 2)
.sum();
Но, чувак, тут не всё так радужно, ёпта. Есть правила, нарушишь — получишь по ебалу гонками данных и прочими радостями.
Первое и главное: не лезь в общую помойку! Твои лямбды должны быть святыми — не трогать ничего снаружи. Иначе получится как в той бане, когда на один таз с водой десять мужиков прыгают.
// ПИЗДЕЦ как делать НЕ НАДО: все лезут в один список
List<String> rezultatPizdec = new ArrayList<>();
spisok.parallelStream()
.forEach(s -> rezultatPizdec.add(s.toUpperCase())); // Ёбнутся все, кто успеет!
// А вот так — по-белому, безопасненько
List<String> rezultatNorm = spisok.parallelStream()
.map(String::toUpperCase)
.collect(Collectors.toList());
Второе: не вызывай духа параллелизма по пустякам.
Если у тебя список из пяти элементов, а операция — просто i + 1, то накладные расходы на организацию этого цирка с потоками съедят всю выгоду. Получится медленнее, чем если бы ты сам, внатуре, посчитал. Это как вызывать такси, чтобы проехать два метра до палатки с пивом.
Третье: а когда оно реально полезно? Ну, когда данных — овердохуища, десятки-сотни тысяч. И когда над каждым элементом нужно провести какую-то ебучую, сложную вычислительную операцию, которая загрузит процессор. Вот тогда да, соседние ядра процессора скажут тебе спасибо.
Четвёртое: а когда это полная хуйня?
Когда твоя операция — это ввод-вывод. Чтение файлов, запросы по сети. Потоки будут тупо ждать ответа, простаивать, а ты только создашь лишнюю суету. Для такого есть другие игрушки, типа CompletableFuture.
И наконец, пятое: если совсем припёрло, можно свой бассейн потоков налить.
По умолчанию используется общий ForkJoinPool.commonPool(). Но если хочешь изолировать свою дичь, чтобы не мешать другим, можно свой создать.
ForkJoinPool moyPersonalniyBasseyn = new ForkJoinPool(4); // 4 работничка
long resultat = moyPersonalniyBasseyn.submit(() ->
spisok.parallelStream()
.mapToInt(...)
.sum()
).get();
Короче, инструмент мощный, но как топор в руках обезьяны. Если бездумно тыкать parallelStream() везде, где видишь, можно не ускорить, а так наворотить, что потом полдня дебажить, почему результаты каждый раз разные. Волнение ебать!