Вопрос 29 · Раздел 16

Что такое projection в JPA

Projection — загрузка только нужных полей сущности вместо всего объекта. Это значительно улучшает производительность, уменьшает объём данных в памяти и устраняет необходимость d...

Версии по языкам: English Russian Ukrainian

Обзор

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 и как её избежать]]