Всем привет! Я работаю разработчиком Java в компании bpm (ранее “ЛАНИТ - Би Пи Эм”). Приглашаю вас погрузиться в увлекательную тему оптимизации запросов в Spring Data — использование проекций. Если вы часто сталкиваетесь с необходимостью выборки лишь нескольких конкретных полей из большой сущности, понимая, насколько ресурсозатратно извлечение всей структуры целиком, этот материал специально для вас.

Приходилось ли вам сталкиваться с ситуацией, когда нужно вычитать только пару полей из сущности, но при этом извлекать всю сущность целиком слишком затратно? Spring Data предоставляет очень мощный инструмент для решения такой задачи – проекции. Благодаря особенностям их реализации мы не будем засорять Persistence Context лишними сущностями, так как извлекаем Unmanaged-объекты. Это дает прирост в производительности. Однако отсюда же растет и ограничение: проекции могут быть использованы только для Read-операций.

Что ж, давайте в блоге ЛАНИТ пошагово рассмотрим:

  • Interface-based проекции,

  • Open and closed проекции,

  • Class-based проекции,

  • динамические проекции.

Interface-based projections

Представим, что у нас есть подопытная сущность:

class Person {
  @Id private UUID id;
  private String firstName;
  private String lastName;
  private String patronymic;
  private String email; 
  private Address address;
}

// Address выглядит примерно так
class Address {
  private String zipCode;
  private String city;
  private String street; 
}

Репозиторий для Person:

interface PersonRepository extends Repository<Person, UUID> {
…
}

Допустим, мы хотим достать из базы данных только имя и фамилию человека без загрузки сущности полностью. Для этого нам понадобится интерфейс с геттерами для полей, которые мы хотим извлечь:

/* Важно! Названия полей проекции должны быть идентичны названиям полей entity, иначе магии не будет. */
interface PersonNamesOnly {
  String getFirstName();
  String getLastName();
}

Модифицируем наш репозиторий:

/* Функционал репозиториев остается прежним, в том числе для проекций можно писать кастомные запросы через @Query. */
interface PersonRepository extends Repository<Person, UUID> {
  …
  Collection<PersonNamesOnly> findByLastname(String lastname);
  …
}

Готово! Теперь при вызове нашего нового метода репозитория Spring будет генерировать прокси под наш интерфейс и извлекать результаты SQL-запроса напрямую в проекцию.

Еще одна фишка Interface-based – поддержка вложенных сущностей:

interface PersonSummary {
  String getFirstname();
  String getLastname();
  AddressSummary getAddress();

// Интерфейс не обязательно должен быть вложенным
  interface AddressSummary {
    String getCity();
  }
}

Open or Closed projections 

Interface-based проекции – «закрытые», то есть их атрибуты должны точно соответствовать сущности, которую они проецируют. 

Но что, если некоторые из полей мы хотим вычислять на основе других полей сущности? 

Есть несколько способов:

  1. дефолтные методы

interface PersonNamesOnly {

  String getFirstname();
  String getLastname();

  default String getFullName() {
    return getFirstname().concat(" ").concat(getLastname());
  }
}

2. SpEL (Spring Expression Language)

interface PersonNamesOnly {
  @Value("#{target.firstname + ' ' + target.lastname}")
  String getFullName();
}

3. кастомные бины для совсем тяжелой или заковыристой логики

@Component
class MyBean {
  String getFullName(Person person) {…}
}

interface PersonNamesOnly {
  @Value("#{@myBean.getFullName(target)}")
  String getFullName(); 
}

Стоит иметь в виду, что открытые проекции менее производительны, так как в SpEL-выражении могут использоваться любые поля сущности, не перечисленные в проекции, и извлечь из БД нужно будет все атрибуты. Но это все равно будет эффективнее, чем извлечение entity-объекта, за счет отсутствия необходимости маппить все поля (заметно на сущностях с десятками полей) и исключения необходимости управлять жизненным циклом объекта (Unmanaged). 

Class-Based projections

Такие проекции, с одной стороны, не похожи на interface-based, а с другой - братья близнецы. 

Основное отличие в том, что мы создаем класс, а не интерфейс:

class PersonDto {
  private String firstname;
  private String lastname;

  public PersonDto(String firstname, String lastname) {...}
}

В репозитории работа с ними тоже несколько отличается:

interface PersonRepository extends Repository<Person, UUID> {
  @Query(SELECT new com.example.PersonDto(u.firstname, u.lastname) FROM User u)
  Collection<PersonDto> findByLastname(String lastname);
}

   /* Важно! Class-based проекции не поддерживают запросы на нативном SQL! */
   /*  Обратите внимание на конструктор в запросе */

Получается, что мы должны сами указать, какие данные в какие поля проекции будут добавлены через конструктор.

В Interface-based это делается за нас неявно. Spring генерирует на основе интерфейса прокси-класс сам.

Сlass-based проекции, в отличие от interface-based, не поддерживают вложенность. Но при этом, благодаря использованию конструктора, названия полей у entity сущности и у проекции могут различаться, главное - расположить параметры в правильном порядке.

Еще одна причина для использования именно class-based проекций – необходимость настроить поля класса, например, добавлением аннотаций в DTO.

Динамические проекции

Заканчиваем обзор технологии динамическими проекциями. Если у нас есть много классов, в которые мы хотели бы трансформировать подопытный объект, то при использовании class-based или interface-based нам пришлось бы для каждого целевого типа писать свой метод. Если методы одинаковые, то это выглядит не очень круто и больше похоже на дублирование.

В таком случае можно использовать динамические проекции:

public interface PersonRepository extends Repository<Person, UUID> {
     ...
    <T> T findByLastName(String lastName, Class<T> type);
    …
}

И затем мы сможем обращаться к ним следующим образом:

Person person = personRepository.findByLastName("Иванов", Person.class);
PersonView personView = personRepository.findByLastName("Петров", PersonView.class);
PersonDto personDto = personRepository.findByLastName("Боширов", PersonDto.class);

Если проекции вызвали интерес, но все еще не до конца понятно, как ими пользоваться, то добро пожаловать в документацию. Буду рад вашему мнению в комментах.

Комментарии (1)


  1. BlAnge
    11.11.2025 07:16

    Спасибо за статью. Подчерпнул и структурировал то, что "и так использовал в проекте методом копипаста".