Всем привет! Я работаю разработчиком 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 проекции – «закрытые», то есть их атрибуты должны точно соответствовать сущности, которую они проецируют.
Но что, если некоторые из полей мы хотим вычислять на основе других полей сущности?
Есть несколько способов:
дефолтные методы
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);
Если проекции вызвали интерес, но все еще не до конца понятно, как ими пользоваться, то добро пожаловать в документацию. Буду рад вашему мнению в комментах.
BlAnge
Спасибо за статью. Подчерпнул и структурировал то, что "и так использовал в проекте методом копипаста".