Совсем недавно стал свидетелем неочевидной проблемы, когда вроде бы полностью протестированный стабильный сервис, по непонятным причинам падает на проде с ошибкой OutOfMemory.
Причинами и способами решения, сегодня я решил поделиться с Вами.
Вводные данные:
Предложенное решение (как показала практика - неверное):
Примерный код:
🧍 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();
};
Проблема:
Вот примеры:
Пример 1
Пример 2
Однако в сочетании с пагинацией (Pageable) Hibernate теряет корректность подсчёта количества строк и может загрузить всю таблицу в память, чтобы затем вручную "отрезать" нужную страницу на уровне Java, тем самым использовав ВСЮ выделенную JVM память для хранения.
О последствиях такого непредсказуемого поведения можете посудить сами.
Что происходит, подробно?
Когда вы вызываете, например:
Page<User> users = userRepository.findAll(specification, PageRequest.of(0, 10));
Hibernate должен выполнить:
SELECT COUNT(*) ... — чтобы узнать общее количество строк.
SELECT ... LIMIT 10 OFFSET 0 — чтобы получить только первую страницу.
Когда вы пишете 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