Java for Beginner
675 subscribers
549 photos
156 videos
12 files
842 links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Как я память искал (Часть I)

Совсем недавно стал свидетелем неочевидной проблемы, когда вроде бы полностью протестированный стабильный сервис, по непонятным причинам падает на проде с ошибкой OutOfMemory.

Причинами и способами решения, сегодня я решил поделиться с Вами
.

Вводные данные:
🔵Основная сущность (к примеру User) и связанная с ней через one-to-many, вторичная сущность (пусть будет Car).
🔵Сущности описаны по стандарту Spring JPA в коде.
🔵По запросу бизнеса, при получении списка основных сущностей должна применяться пагинация, фильтрация и сортировка (для корректного отображения на web-странице).
🔵Само собой при получении основных сущностей, нужно подгружать все связанные, а так же иметь возможность фильтрации и сортировки по содержащихся в них данных.

Предложенное решение (как показала практика - неверное):
🔵Для решения подобного запроса использовать стандартные средства Spring, такие как Pageable и для формирования сложного SQL запроса средствами Java - Specification

Примерный код:

🧍 User
@Entity
public class User {
//стандартные поля
@OneToMany(mappedBy = "owner", fetch = FetchType.LAZY)
private List<Car> cars = new ArrayList<>();
}


🚗 Car
@Entity
public class Car {
//стандартные поля
@ManyToOne
@JoinColumn(name = "owner_id")
private User owner;
}


Пагинация + fetch join через Specification (без фильтров)

Page<User> users = userRepository.findAll(specification, PageRequest.of(0, 10));


настройка Specification
return (root, query, cb) -> {
root.fetch("cars", JoinType.LEFT);
query.distinct(true);
return cb.conjunction();
};


Проблема:
🔵Хотя предложенное выше решение и выглядит очевидным при озвученном кейсе требований и решении проблемы N+1, оно создает проблему о которой Вам не расскажут на хайповых курсах и видео уроках.

Вот примеры:
Пример 1
Пример 2

🔵Суть проблемы в том, что когда вы используете JOIN FETCH (например, через Specification), Hibernate подгружает связанные сущности в один SQL-запрос (чтобы избежать N+1).

Однако в сочетании с пагинацией (Pageable) Hibernate теряет корректность подсчёта количества строк и может загрузить всю таблицу в память, чтобы затем вручную "отрезать" нужную страницу на уровне Java, тем самым использовав ВСЮ выделенную JVM память для хранения
.

О последствиях такого непредсказуемого поведения можете посудить сами. 😱


Что происходит, подробно?

Когда вы вызываете, например:
Page<User> users = userRepository.findAll(specification, PageRequest.of(0, 10));


Hibernate должен выполнить:
SELECT COUNT(*) ... — чтобы узнать общее количество строк.
SELECT ... LIMIT 10 OFFSET 0 — чтобы получить только первую страницу.


❗️Но fetch join меняет семантику запроса

Когда вы пишете JOIN FETCH, например:
root.fetch("cars", JoinType.LEFT);

Или:
criteriaQuery.distinct(true);


Hibernate генерирует SQL примерно такого вида:
SELECT u.*, r.* FROM users u
LEFT JOIN roles r ON r.user_id = u.id


При этом:
Если у одного пользователя 3 cars, то он появится 3 раза в результате SQL-запроса.
Hibernate потом вручную собирает дубликаты в одну сущность User, у которой будет List<Car> с 3 элементами.
LIMIT/OFFSET применяются к строкам SQL, а не к "собранным" сущностям — и это вызывает проблемы.


⚠️ Проблема: LIMIT работает до агрегации

Hibernate не может корректно объединить дубликаты после применения LIMIT, потому что:

При использовании fetch join, результат SQL-разворачивается в несколько строк (по связям).
Но LIMIT обрезает эти строки до того, как Hibernate агрегирует их в объекты Java.
Поэтому Hibernate игнорирует LIMIT в SQL, чтобы корректно собрать сущности → он загружает все строки в память, затем отрезает нужную страницу на уровне Java.


🤯 Что я получил в результате использования неверного решения

🔜 Упавший сервис на проде 😨 (так как на тестовых средах, объем загружамых данных был в разы меньше).
🔜 Бесценный опыт, которым сейчас делюсь с Вами ☺️

А завтра я расскажу как решить данный кейс и как не попасть в подобную ловушку неочевидного поведения Hibernate ...

Понравился стиль подачи материала?

Отправь другу и ставь -
🔥

#Java #fetch #autor
Please open Telegram to view this post
VIEW IN TELEGRAM