Что такое projection в JPA
Projection — загрузка только нужных полей сущности вместо всего объекта. Это значительно улучшает производительность, уменьшает объём данных в памяти и устраняет необходимость d...
Обзор
Projection — загрузка только нужных полей сущности вместо всего объекта. Это значительно улучшает производительность, уменьшает объём данных в памяти и устраняет необходимость dirty checking для read-only операций.
🟢 Junior Level
Что такое projection
Projection — загрузка части полей вместо всей сущности.
// Вся сущность — все поля
SELECT u FROM User u
// Projection — только нужные поля
SELECT u.name, u.email FROM User u
Зачем использовать
- Меньше данных из БД → быстрее запрос
- Меньше данных в памяти → экономия
- Нет dirty checking → меньше overhead
- Контролируемый контракт API
Простой пример
// DTO класс
public record UserDto(String name, String email) {}
// JPQL с конструктором
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u")
List<UserDto> findUserDtos();
🟡 Middle Level
Типы projection
1. Array projection
@Query("SELECT u.name, u.email FROM User u")
List<Object[]> findNamesAndEmails();
// Использование
List<Object[]> results = repository.findNamesAndEmails();
for (Object[] row : results) {
String name = (String) row[0];
String email = (String) row[1];
}
Минусы: type-unsafe, неудобно
2. DTO projection (constructor expression)
public record UserDto(String name, String email) {}
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u")
List<UserDto> findUserDtos();
Плюсы: type-safe, удобно
3. Interface-based (Spring Data)
interface UserNameOnly {
String getName();
String getEmail();
}
List<UserNameOnly> findBy();
Плюсы: Spring создаёт proxy через интерфейс и маппит колонки по имени getter-а. getName() → колонка “name”. Если имя не совпадает — null.
4. Class-based (Spring Data)
public class UserDto {
private String name;
private String email;
public UserDto(String name, String email) {
this.name = name;
this.email = email;
}
}
List<UserDto> findBy();
Примеры
// Агрегация
@Query("SELECT new com.example.UserStats(u.name, COUNT(o), SUM(o.total)) " +
"FROM User u LEFT JOIN u.orders o GROUP BY u.id")
List<UserStats> findUserStats();
// С условием
@Query("SELECT new com.example.OrderSummary(o.id, o.status, u.name) " +
"FROM Order o JOIN o.user u WHERE o.createdAt > :date")
List<OrderSummary> findRecentOrders(@Param("date") LocalDate date);
Типичные ошибки
// ❌ Без полного имени класса
@Query("SELECT new UserDto(u.name, u.email) FROM User u") // ❌
// ✅ С полным именем
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u") // ✅
// ❌ Неправильный порядок аргументов
public record UserDto(String email, String name) {} // email, name
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u") // ❌ name, email
// ✅ Порядок должен совпадать
public record UserDto(String name, String email) {}
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u") // ✅
🔴 Senior Level
Performance сравнение
Full entity:
- SELECT * → загрузка всех полей
- Dirty checking overhead
- L1 cache storage
- O(N) памяти на entity
Projection:
- SELECT specific columns
- Нет dirty checking
- Меньше памяти
- O(1) памяти на DTO
Разница:
Projection значительно экономит память (загружаются только нужные колонки) и ускоряет запрос (меньше данных по сети). Точные цифры зависят от entity size, БД и сети.
Продвинутые паттерны
// Pattern 1: Nested DTO
public record OrderDto(
Long id,
String status,
UserDto user,
List<ItemDto> items
) {}
@Query("""
SELECT new com.example.OrderDto(
o.id, o.status,
new com.example.UserDto(u.name, u.email),
// items можно загрузить вторым запросом через @EntityGraph или
// отдельным query: SELECT i FROM Item i WHERE i.orderId IN (:orderIds)
null
)
FROM Order o JOIN o.user u
WHERE o.id = :id
""")
OrderDto findByIdWithUser(@Param("id") Long id);
// Pattern 2: Tuple-based projection
@Query("SELECT u.name as name, u.email as email FROM User u")
List<Tuple> findUsers();
List<UserDto> dtos = tuples.stream()
.map(t -> new UserDto(t.get("name", String.class),
t.get("email", String.class)))
.toList();
// Pattern 3: Dynamic projection with Criteria API
public <T> List<T> findUsers(Class<T> type) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<T> query = cb.createQuery(type);
Root<User> root = query.from(User.class);
if (type == UserDto.class) {
query.select(cb.construct(UserDto.class,
root.get("name"), root.get("email")));
} else {
query.select(root);
}
return em.createQuery(query).getResultList();
}
Когда использовать projection
✅ Projection для:
- Read-only операций
- API ответов
- Списков/таблиц
- Агрегации и статистики
- Больших наборов данных
❌ Полная сущность для:
- CRUD операций
- Когда нужны все поля
- Когда нужен dirty checking
- Маленьких наборов данных
Best Practices
✅ DTO projection для read-only
✅ Interface-based для гибкости
✅ Class-based (record) для type-safety
✅ Полное имя класса в JPQL
✅ Правильный порядок аргументов
❌ Array projection (type-unsafe)
❌ Full entity для read-only
❌ Без полного имени класса
❌ Неправильный порядок конструктора
🎯 Шпаргалка для интервью
Обязательно знать:
- Projection — загрузка только нужных полей вместо всей сущности
- 4 типа: Array (type-unsafe), DTO constructor (type-safe), Interface-based (Spring), Class-based (Spring)
- Преимущества: меньше данных из БД, нет dirty checking overhead, контролируемый API контракт
- DTO projection требует полное имя класса: new com.example.UserDto(…)
- Порядок аргументов в конструкторе должен совпадать с порядком в SELECT
- Для read-only/API — projection, для CRUD — полные сущности
Частые уточняющие вопросы:
- DTO vs Interface-based projection? DTO (record) — type-safe, явный конструктор; Interface — Spring создаёт proxy, getter mapping по имени
- Почему Array projection плох? Type-unsafe: row[0], row[1] — ошибки при рефакторинге, неудобно
- Performance разница? Projection экономит память (только нужные колонки) и ускоряет запрос (меньше данных по сети)
- Nested DTO можно? Да — new com.example.OrderDto(new UserDto(u.name), …) в JPQL
Красные флаги (НЕ говорить):
- «Array projection для production кода» — type-unsafe, row[0] легко сломать
- «Без полного имени класса в JPQL» — ClassNotFoundException
- «Полная сущность для read-only API» — лишний dirty checking, больше памяти
- «Неправильный порядок конструктора» — данные перепутаны, name → email
Связанные темы:
- [[26. Что такое JPQL и чем он отличается от SQL]]
- [[27. Что такое Criteria API и когда его использовать]]
- [[25. Как избежать бесконечной рекурсии при сериализации Entity]]
- [[4. Что такое LazyInitializationException и как её избежать]]