Самые пугающие три точки кода.
💬 Varargs в Java 💬
Сегодня в рабочем проекте столкнулся с методом, в аргументах которого было выражение типа Object... data.
Сразу подумал, что скорее всего на канале, этот момент не рассматривался. Нашел в коротком варианте, опубликованном аж 6 июня 2024 года.
Решил немного расширить взгляд на такой вариант передачи данных в метод.
Как это работает? Теория.🤓
Когда метод принимает аргументы через Varargs, то компилятор при этом неявно преобразует такой параметр в массив типа Object[].
по сути компилируется как
При вызове метода с varargs компилятор сам создаёт массив нужного размера и заполняет его переданными значениями (даже если аргументов нет, создаётся пустой массив).
❗️ При этом varargs-параметр может быть только последним в списке и быть единственным varargs в методе ❗️
Как это работает? Практика.🧑💻
Пример метода, принимающего переменное число строк:
Или самый любимый вариант когда метод printAll(Object... args) может принимать любой набор объектов:
Здесь в args будут храниться объекты типа String, Integer, Boolean, User и т.д. Встроенный метод String.format тоже использует varargs: его сигнатура String.format(Locale, String, Object... args) позволяет передавать любое количество различных объектов в шаблон.
С каким💩 можно столкнуться если увлекаться Varargs. ⛔️
Выборки из stackoverflow.com:
И личный совет: varargs удобны, но не злоупотребляйте ими. Старайтесь держать код ясным: ограничивайте использование varargs реальными случаями, где это упрощает интерфейс, и следите за типобезопасностью.
Понравился стиль подачи материала?
Отправь другу и ставь - 🔥
#Java #varargs #autor #для_новичков #junior
Сегодня в рабочем проекте столкнулся с методом, в аргументах которого было выражение типа Object... data.
Сразу подумал, что скорее всего на канале, этот момент не рассматривался. Нашел в коротком варианте, опубликованном аж 6 июня 2024 года.
Решил немного расширить взгляд на такой вариант передачи данных в метод.
Поддержка переменного числа аргументов (varargs) была добавлена в Java, начиная с версии JDK 5 (Java 5). До этого программисту приходилось либо перегружать методы для разного количества параметров, либо передавать массив аргументов вручную.
Иными словами - если у Вас возникает случай, что в метод может поступать неопределенное количество параметров, в том числе и разного типа, но метод должен быть только ОДИН - Varargs Ваш выбор 🤙
Как это работает? Теория.
Когда метод принимает аргументы через Varargs, то компилятор при этом неявно преобразует такой параметр в массив типа Object[].
void foo(Object... data) { /*...*/ }
по сути компилируется как
void foo(Object[] data) { /*...*/ }
При вызове метода с varargs компилятор сам создаёт массив нужного размера и заполняет его переданными значениями (даже если аргументов нет, создаётся пустой массив).
Как это работает? Практика.
Пример метода, принимающего переменное число строк:
public static void printStrings(String... words) {
for (String w : words) {
System.out.print(w + " ");
}
System.out.println();
}
// Вызовы:
printStrings("Alice", "Bob");
printStrings("one", "two", "three");
При этом внутри words будет String[] со всеми переданными значениями
Или самый любимый вариант когда метод printAll(Object... args) может принимать любой набор объектов:
public static void printAll(Object... args) {
for (Object o : args) {
System.out.print(o + " ");
}
System.out.println();
}
// Вызов:
printAll("Hello", 123, true, new User("Tom"));
Здесь в args будут храниться объекты типа String, Integer, Boolean, User и т.д. Встроенный метод String.format тоже использует varargs: его сигнатура String.format(Locale, String, Object... args) позволяет передавать любое количество различных объектов в шаблон.
С каким
Выборки из stackoverflow.com:
Приведение типов и автопакетирование
Метод void m(Object... args) под капотом видит args как Object[]. Если вы ожидаете внутри определённый тип, будьте осторожны с приведением, иначе может возникнуть ClassCastException. Попытка привести Object[] к String[] при неправильном использовании varargs приведёт к ошибке времени выполнения.
Ещё один нюанс – работа с примитивными типами и их обёртками: varargs не применяет автопакетирование к массивам примитивов. То есть если вы ожидаете набор чисел, стоит использовать Integer... вместо int..., иначе передача int[] в параметр типа Object... будет воспринята как единичный объект int[], а не как набор чисел.
Конфликт с перегрузкой методов
Перегрузка методов вместе с varargs может привести к неоднозначности. Так, если объявлены оба метода fun(int... a) и fun(boolean... b), то вызов fun() без аргументов компилятор посчитает неразрешимым, ведь он не может однозначно выбрать, какую версию вызывать.
Аналогично, комбинация fun(int... a) и fun(int n, int... a) приводит к конфликту при вызове fun(1), потому что компилятор не знает, вызвать ли первый метод с одним элементом или второй метод с фиксированным первым параметром. Такие неоднозначности приводят к ошибкам компиляции. Чтобы этого избежать, не перегружайте методы только за счёт varargs: лучше дать методам разные имена или явно менять количество параметров.
И личный совет: varargs удобны, но не злоупотребляйте ими. Старайтесь держать код ясным: ограничивайте использование varargs реальными случаями, где это упрощает интерфейс, и следите за типобезопасностью.
Понравился стиль подачи материала?
Отправь другу и ставь - 🔥
#Java #varargs #autor #для_новичков #junior
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6🤬1 1
Совсем недавно стал свидетелем неочевидной проблемы, когда вроде бы полностью протестированный стабильный сервис, по непонятным причинам падает на проде с ошибкой 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
🔥9
В предыдущей части мы рассмотрели составные части возникновения проблемы.
А теперь давайте рассмотрим, как решить данный кейс и как не попасть в подобную ловушку неочевидного поведения Hibernate.
Решения
Избегать fetch join при пагинации в Hibernate
Да так просто.
Но вы скажете: "А как же проблема N+1"? Ее я предлагаю решить всем известной аннотацией @BatchSize.
@Entity
public class User {
//стандартные поля
@OneToMany(mappedBy = "owner")
@BatchSize(size = 50)
private List<Car> cars = new ArrayList<>();
}
Количество запросов существенно уменьшится — на тот размер size, который мы задали. Это значит, что проблема N+1 решена.
Да, запросов станет точно больше чем 1. Но такова цена использования Spring Hibernate
разделение запроса на две части с использованием @EntityGraph
@EntityGraph(attributePaths = {"cars"})
Page<User> findAll(Pageable pageable);
При таком решении в первой части запроса мы запрашиваем id наших User с определенной страницы. А во второй части запроса с помощью @EntityGraph и JOIN мы получаем все Car которые им принадлежат.
Но как показали тестовые запуски с использованием @EntityGraph над множеством связанных сущностей в основной, проведенные автором статьи(спасибо ему) :
"При пяти загружаемых коллекциях производительность разделенного запроса с @EntityGraph хуже в 2 раза. При десяти — в 45 раз. А при загрузке 15 коллекций — в 1401 раз! На графике нет данных по разделенному запросу для 20 коллекций, так как я просто-напросто получил ошибку OutOfMemoryError."
С @BatchSize же никаких JOIN не происходит, и при добавлении дополнительной коллекции с ассоциацией @OneToMany просто добавляются дополнительные select для этой коллекции.
Использовать Нативный SQL - запрос.
Да, больше мороки с формированием динамического SQL (если у тебя 10 разных фильтров, ты сам будешь собирать SQL строку).
@Query(value = """
SELECT u.id AS userId, u.name AS userName, c.model AS carModel
FROM user u
LEFT JOIN car c ON c.owner_id = u.id
WHERE (:name IS NULL OR u.name ILIKE %:name%)
ORDER BY
CASE WHEN :sortBy = 'name' THEN u.name
WHEN :sortBy = 'id' THEN CAST(u.id AS TEXT)
ELSE u.name
END ASC
LIMIT :limit OFFSET :offset
""", nativeQuery = true)
List<UserCarDTO> findWithFilters(
@Param("name") String name,
@Param("sortBy") String sortBy,
@Param("limit") int limit,
@Param("offset") int offset
);
Да, придется использовать дополнительный count-запрос: SELECT COUNT(*) , для обеспечения пагинации
@Query(value = """
SELECT COUNT(DISTINCT u.id)
FROM user u
WHERE (:name IS NULL OR u.name ILIKE %:name%)
""", nativeQuery = true)
long countUsers(@Param("name") String name);
но, ты сам полностью управляешь процессом и скорее всего не встретишь неочевидного поведения
Какие ещё конструкции в JPA/Hibernate могут привести к аналогичным проблемам, когда ты используешь fetch join и неожиданно получаешь избыточную загрузку в память, ломающую пагинацию или вызывающую дублирование/нагрузку?
1. Pageable + @Query (с JPQL) и JOIN FETCH
Даже если ты не используешь Specification, а пишешь JPQL с @Query, проблема остаётся.
2. EntityManager + JPQL с fetch и пагинацией
Даже ручной вызов через EntityManager может привести к тому же эффекту.
3. Criteria API с fetch + .setMaxResults()/.setFirstResult()
Даже если ты используешь чистый JPA Criteria API, проблема может проявиться.
4. Spring Data REST + @RepositoryRestResource + fetch join
Если ты используешь Spring Data REST, и в репозитории включён fetch join, например через @Query, то Spring сам применяет Pageable, и может попасть в эту ловушку. Опять же — всё сломается.
И личный совет: изучайте матчасть! Читайте наш канал и шанс, что Ваш прод упадет - существенно снизится!
Понравился стиль подачи материала?
Отправь другу и ставь - 🔥
#Java #fetch #autor
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6
Пагинация, которую начинаешь ненавидеть 😵
Сегодня вновь хочу рассказать о неочевидном поведении Spring Data JPA, которое хоть и встречается нечасто, но может попортить нервы.
Давайте представим, что есть связанные сущности(код условный, никакого реального совпадения):
И есть бизнес-задача: Провести сортировку (SortBy в Pageable) по полям вложенных сущностей (т.е. по workstation.number и department.name).
🙅♂️ Как хочется сделать
Как минимум все по стандарту:
И выглядит вроде бы все пристойно.
Но! Как всегда в магии Spring'а есть НО✌️
Для связи @OneToOne (Workstation.number) сортировка работает, потому что это прямое простое соединение (join), и JPA может легко построить корректный SQL-запрос:
❗️ Но, для связи @OneToMany или @ManyToOne (department.name) сортировка не работает❗️
Потому что:
Это коллекционная ассоциация
При соединении таблиц может возникать несколько записей для одной основной сущности
JPA/Hibernate не может автоматически разрешить, как именно нужно применять сортировку в таком случае и не делает НИЧЕГО🙄
🆗 Как надо сделать
Явный JOIN + ORDER BY в JPQL
Явное указание, как соединять таблицы и ORDER BY до пагинации как минимум решат все проблемы🏝
Использование JOIN FETCH (если нужны данные сразу)
Тот же вариант что и выше, но если не жалко памяти🆘
Specification API
Вариант для любителей пожесче🧑💻
Как не сойти с ума при поиске подобных ошибок?🤪
Всегда, на стадии разработки и тестирования включайте SQL в логах (spring.jpa.show-sql=true) и будет Вам счастие.
Помните - если сортировка не применяется – валите всё на JOIN.
Понравился стиль подачи материала?
Отправь другу и ставь - 🔥
Если бывало такое -❤️
#Java #join #autor
Сегодня вновь хочу рассказать о неочевидном поведении Spring Data JPA, которое хоть и встречается нечасто, но может попортить нервы.
Давайте представим, что есть связанные сущности(код условный, никакого реального совпадения):
@Entity
@Data
public class Employee {
@Id
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "workstation_id")
private Workstation workstation;
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
}
@Entity
@Data
public class Workstation {
@Id
private Long id;
private String number; // Поле для сортировки
}
@Entity
@Data
public class Department {
@Id
private Long id;
private String name; // Поле для сортировки
}
И есть бизнес-задача: Провести сортировку (SortBy в Pageable) по полям вложенных сущностей (т.е. по workstation.number и department.name).
Как минимум все по стандарту:
@Query("SELECT e FROM Employee e WHERE e.name = :name")
Page<Employee> findByName(@Param("name") String name, Pageable pageable)
Естественно поля могут передаваться таким образом:
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.fromString("workstation.number"));
или
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.fromString("department.name"));
И выглядит вроде бы все пристойно.
Но! Как всегда в магии Spring'а есть НО
Для связи @OneToOne (Workstation.number) сортировка работает, потому что это прямое простое соединение (join), и JPA может легко построить корректный SQL-запрос:
SELECT e.* FROM employee e
JOIN workstation w ON e.workstation_id = w.id
WHERE e.name = 'John'
ORDER BY w.number ASC -- Сортировка применяется
Потому что:
Это коллекционная ассоциация
При соединении таблиц может возникать несколько записей для одной основной сущности
JPA/Hibernate не может автоматически разрешить, как именно нужно применять сортировку в таком случае и не делает НИЧЕГО
Явный JOIN + ORDER BY в JPQL
@Query("SELECT e FROM Employee e " +
"LEFT JOIN e.department d " + // Явное соединение
"WHERE e.name = :name " +
"ORDER BY d.name") // Сортировка в запросе(но не обязательно)
Page<Employee> findByNameWithDepartmentSort(
@Param("name") String name,
Pageable pageable);
Явное указание, как соединять таблицы и ORDER BY до пагинации как минимум решат все проблемы
Использование JOIN FETCH (если нужны данные сразу)
@Query("SELECT DISTINCT e FROM Employee e " +
"LEFT JOIN FETCH e.department d " + // Загружаем department сразу
"WHERE e.name = :name " +
"ORDER BY d.name")
Page<Employee> findByNameWithDepartmentFetch(
@Param("name") String name,
Pageable pageable);
Тот же вариант что и выше, но если не жалко памяти
Specification API
public interface EmployeeRepository extends
JpaRepository<Employee, Long>,
JpaSpecificationExecutor<Employee> { }
// В сервисе:
Specification<Employee> spec = (root, query, cb) -> {
Join<Employee, Department> department = root.join("department");
return cb.equal(root.get("name"), name);
};
Sort sort = Sort.by("department.name").ascending();
Pageable pageable = PageRequest.of(0, 10, sort);
Page<Employee> result = repository.findAll(spec, pageable);
Вариант для любителей пожесче
Как не сойти с ума при поиске подобных ошибок?
Всегда, на стадии разработки и тестирования включайте SQL в логах (spring.jpa.show-sql=true) и будет Вам счастие.
Помните - если сортировка не применяется – валите всё на JOIN.
Понравился стиль подачи материала?
Отправь другу и ставь - 🔥
Если бывало такое -
#Java #join #autor
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6👎1