Що таке projection в JPA
Проєкція (Projection) у JPA та Spring Data — це механізм вибірки суворо визначеної підмножини полів або обчислюваних виразів замість завантаження повної сутності. Використання п...
Проєкція (Projection) у JPA та Spring Data — це механізм вибірки суворо визначеної підмножини полів або обчислюваних виразів замість завантаження повної сутності. Використання проєкцій — один із найпотужніших способів оптимізації продуктивності: воно скорочує мережевий трафік між СУБД та JVM, економить оперативну пам’ять купи (Heap) і повністю виключає накладні витрати Hibernate на відстеження змін (Dirty Checking) та керування контекстом персистентності (PersistenceContext).
🟢 Junior Level
Навіщо потрібні проєкції
Коли ви виконуєте стандартний запит SELECT u FROM User u, Hibernate:
- Виконує
SELECT *з усіма колонками таблиці (включно з важкими текстовими описами, аватарами та метаданими). - Створює повноцінний екземпляр Java-об’єкта
User. - Зберігає точний дублікат стану в знімок
loadedStateкешу L1 для відстеження Dirty Checking. - Прив’язує до об’єкта проксі-обробники зв’язків.
Якщо вам потрібно лише відобразити ім’я та email у таблиці користувачів, 90% цієї роботи — марне витрачання ресурсів. Проєкція запитує лише те, що потрібно:
// Проєкція в Java Record:
public record UserSummary(String name, String email) {}
// JPQL через конструктор (Constructor Expression):
TypedQuery<UserSummary> query = entityManager.createQuery("""
SELECT new com.example.dto.UserSummary(u.name, u.email)
FROM User u
""", UserSummary.class);
List<UserSummary> summaries = query.getResultList();
// Згенерований SQL: SELECT name, email FROM users;
// Жодного L1 кешу, жодних знімків, мінімальний трафік!
🟡 Middle Level
5 різновидів проєкцій у JPA та Spring Data
1. Нетипізований масив Object[] (Raw Array)
List<Object[]> rows = entityManager.createQuery("SELECT u.name, u.email FROM User u").getResultList();
String name = (String) rows.get(0)[0]; // Небезпечно: легко помилитися індексом
Мінуси: Відсутність типобезпечності, схильність до зламу при рефакторингу.
2. Проєкція на базі jakarta.persistence.Tuple
Дозволяє звертатися до колонок за рядковими псевдонімами (псевдо-типобезпечно):
List<Tuple> tuples = entityManager.createQuery("""
SELECT u.name AS userName, u.email AS userEmail FROM User u
""", Tuple.class).getResultList();
String name = tuples.get(0).get("userName", String.class);
3. DTO Constructor Expression (SELECT new ...)
Класичний JPA-стандарт. Працює як зі звичайними класами, так і з Java Records:
public record UserInfoDto(Long id, String email) {}
@Query("SELECT new com.example.dto.UserInfoDto(u.id, u.email) FROM User u")
List<UserInfoDto> fetchUsers();
Обов’язкова умова: У JPQL необхідно вказувати повне ім’я класу з пакетом (new com.example.dto.UserInfoDto(...)).
4. Закриті інтерфейсні проєкції Spring Data (Closed Projections)
Spring Data дозволяє оголосити простий інтерфейс із гетерами:
public interface UserContactView {
String getName();
String getEmail();
}
public interface UserRepository extends JpaRepository<User, Long> {
List<UserContactView> findAllByActiveTrue();
}
Spring Data аналізує гетери інтерфейсу і генерує оптимальний SQL, що запитує лише колонки name та email.
5. Динамічні проєкції (Dynamic Projections)
Один метод репозиторію може повертати різні проєкції залежно від переданого класу:
public interface UserRepository extends JpaRepository<User, Long> {
<T> List<T> findByStatus(String status, Class<T> type);
}
// Використання:
List<UserSummary> list1 = repo.findByStatus("ACTIVE", UserSummary.class);
List<UserDetailDto> list2 = repo.findByStatus("ACTIVE", UserDetailDto.class);
🔴 Senior Level
Пастка відкритих проєкцій (Open Projections) у Spring Data
Spring Data підтримує так звані «відкриті проєкції» через SpEL-вирази (Spring Expression Language):
// ❌ ПРИХОВАНА ДЕГРАДАЦІЯ ПРОДУКТИВНОСТІ:
public interface UserFullNameView {
@Value("#{target.firstName + ' ' + target.lastName}")
String getFullName();
}
Чому це небезпечно:
- При Closed Projection (
String getFirstName()) Spring Data оптимізує SQL:SELECT first_name FROM users. - При Open Projection з
@ValueSpring Data не може знати, які саме поля знадобляться виразу SpEL під час обчислення. - Наслідок: Spring Data змушений виконати повний
SELECT *, вивантажити всю сутність з усіма колонками в пам’ять, створити JDK Dynamic Proxy і лише потім обчислити вираз у JVM! Виграш у продуктивності обнуляється.
Проєкції та колекції: Обмеження специфікації JPA
Специфікація JPA прямо забороняє передавати колекції в конструкторні проєкції:
// ❌ ПОМИЛКА КОМПІЛЯЦІЇ JPQL:
public record OrderWithItemsDto(Long orderId, List<ItemDto> items) {}
// Спроба написати таке завершиться помилкою парсингу:
@Query("SELECT new com.example.OrderWithItemsDto(o.id, o.items) FROM Order o")
List<OrderWithItemsDto> findOrders();
Як правильно проєктувати відносини $1:N$:
- Підхід у два запити: Завантажити DTO замовлень, потім окремим запитом завантажити DTO позицій із
WHERE order_id IN (:ids)і згрупувати в пам’яті черезCollectors.groupingBy(). - Використання Hibernate
TupleTransformer: У Hibernate 6 перетворити плаский результатJOINв ієрархічний DTO за допомогою кастомногоResultListTransformer. - Використання бібліотеки Blaze-Persistence: Розширення поверх JPA, що офіційно підтримує вкладені DTO-колекції прямо в запитах.
4 Tricky Questions
1. Чи будуть сутності або DTO, отримані через конструкторну проєкцію (SELECT new UserDto(...)), відстежуватися механізмом Dirty Checking?
Відповідь:
- Категорично ні.
- Об’єкти DTO не є сутностями JPA (
@Entity). Вони інстанціюються через звичайний виклик Java-конструктораnew UserDto(...)прямо із сирих колонокResultSet. - Вони не реєструються в карті
StatefulPersistenceContext.entitiesByKeyі не створюють записівEntityEntry. - Будь-які зміни полів у такому DTO не призведуть до генерації SQL
UPDATEпри коміті транзакції.
2. Чому у високонавантажених системах (HighLoad) використання Java Records переважніше за інтерфейсні проєкції Spring Data?
Відповідь:
- Для інтерфейсних проєкцій Spring Data на кожен рядок результату створює динамічний JDK-проксі (
java.lang.reflect.Proxy), що огортаєMap<String, Object>. - Створення проксі та звернення до гетерів через Reflection створює відчутний оверхед по CPU та алокації пам’яті при вибірках від 10 000 рядків.
- Java Records компілюються в нативний байткод. Створення рекорд-об’єкта через конструктор виконується миттєво з нульовим рефлексивним оверхедом та оптимальним пакуванням пам’яті в купі JVM.
3. Чи можна об’єднати пагінацію (Pageable) та інтерфейсну проєкцію в Spring Data JPA репозиторії?
Відповідь:
- Так, повністю підтримується.
- Метод
Page<UserView> findByActive(boolean active, Pageable pageable)коректно генерує два запити:SELECT COUNT(id) FROM users WHERE active = ?(для підрахунку загальної кількості сторінок).SELECT id, name, email FROM users WHERE active = ? LIMIT ? OFFSET ?(точкова вибірка потрібних колонок сторінки).
4. Що станеться, якщо в класі DTO типи аргументів конструктора не збіжаться за типами даних із колонками JPQL запиту (наприклад, Long у конструкторі проти Integer із бази)?
Відповідь:
- Hibernate намагається знайти конструктор із точним збігом типів аргументів через Java Reflection.
- Автоматичного розширення примітивних типів (наприклад, приведення
IntegerдоLongабоBigDecimalдоDouble) рушій JPA не робить. - Якщо конструктор із сигнатурою, що точно відповідає типам виразів
SELECT, не знайдено, запит впаде в Runtime із винятком:org.hibernate.query.SemanticException: Unable to locate appropriate constructor on class [com.example.UserDto].
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«Проєкції в JPA дозволяють вибирати тільки необхідні поля замість завантаження повної сутності з усіма колонками. Це знижує мережевий трафік, економить пам’ять купи JVM і виключає накладні витрати на Dirty Checking у кеші L1. Основні способи реалізації: DTO Constructor Expressions (через Java Records
SELECT new ...) та закриті інтерфейси Spring Data. Важливий senior-нюанс: відкриті інтерфейсні проєкції зі SpEL-анотацією@Valueнівелюють оптимізацію, оскільки Spring змушений вивантажувати всю сутність цілком. Для read-only операцій та віддачі даних у REST API проєкції в Java Records є абсолютним стандартом індустрії».
Ключові метрики для Production-моніторингу
- Allocated Memory per Request (GC Allocation Rate): заміна повних сутностей на DTO-проєкції знижує виділення пам’яті в 3–5 разів.
- Database Network Traffic / Egress Bytes: зменшення обсягу мережевого трафіку між сервером застосунків та СУБД.
Червоні прапорці (Чого говорити НЕ МОЖНА)
- ❌ «Проєкції оновлюють базу даних через dirty checking» — DTO не керуються сесією Hibernate, dirty checking для них не працює.
- ❌ «Використовую відкриті проєкції з @Value(“#{target…}”) заради максимальної продуктивності» — Навпаки,
@Valueвикачує весь рядок таблиці цілком. - ❌ «У конструкторну проєкцію можна легко передати вкладену колекцію @OneToMany» — Заборонено JPA, призведе до помилки компіляції запиту.
- ❌ «Для read-only звітів краще завантажувати повні JPA-сутності» — Грубий антипатерн, що призводить до перевантаження пам’яті та ризику N+1.