Перейти к основному содержимому

Прогнозы Spring Data JPA

· 6 мин. чтения

1. Обзор

При использовании Spring Data JPA для реализации уровня сохраняемости репозиторий обычно возвращает один или несколько экземпляров корневого класса. Однако чаще всего нам не нужны все свойства возвращаемых объектов.

В таких случаях мы можем захотеть получить данные как объекты настраиваемых типов. Эти типы отражают частичные представления корневого класса, содержащие только интересующие нас свойства. Вот где проекции пригодятся.

2. Первоначальная настройка

Первым шагом является настройка проекта и заполнение базы данных.

2.1. Зависимости Maven

Чтобы узнать о зависимостях, ознакомьтесь с разделом 2 этого руководства .

2.2. Классы сущностей

Давайте определим два класса сущностей:

@Entity
public class Address {

@Id
private Long id;

@OneToOne
private Person person;

private String state;

private String city;

private String street;

private String zipCode;

// getters and setters
}

А также:

@Entity
public class Person {

@Id
private Long id;

private String firstName;

private String lastName;

@OneToOne(mappedBy = "person")
private Address address;

// getters and setters
}

Отношения между сущностями Person и Address являются двунаправленными один к одному; Address — это сторона-владелец, а Person — обратная сторона.

Обратите внимание, что в этом руководстве мы используем встроенную базу данных H2.

Когда встроенная база данных настроена, Spring Boot автоматически создает базовые таблицы для определенных нами сущностей.

2.3. SQL-скрипты

Мы будем использовать сценарий projection-insert-data.sql для заполнения обеих резервных таблиц:

INSERT INTO person(id,first_name,last_name) VALUES (1,'John','Doe');
INSERT INTO address(id,person_id,state,city,street,zip_code)
VALUES (1,1,'CA', 'Los Angeles', 'Standford Ave', '90001');

Чтобы очищать базу данных после каждого запуска теста, мы можем использовать другой скрипт, projection-clean-up-data.sql :

DELETE FROM address;
DELETE FROM person;

2.4. Тестовый класс

Затем, чтобы подтвердить, что проекции дают правильные данные, нам нужен тестовый класс:

@DataJpaTest
@RunWith(SpringRunner.class)
@Sql(scripts = "/projection-insert-data.sql")
@Sql(scripts = "/projection-clean-up-data.sql", executionPhase = AFTER_TEST_METHOD)
public class JpaProjectionIntegrationTest {
// injected fields and test methods
}

С заданными аннотациями Spring Boot создает базу данных, внедряет зависимости, а также заполняет и очищает таблицы до и после выполнения каждого тестового метода.

3. Проекции на основе интерфейса

При проектировании объекта естественно полагаться на интерфейс, поскольку нам не нужно предоставлять реализацию.

3.1. Закрытые прогнозы

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

Объявим проекционный интерфейс для класса Address :

public interface AddressView {
String getZipCode();
}

Затем мы будем использовать его в интерфейсе репозитория:

public interface AddressRepository extends Repository<Address, Long> {
List<AddressView> getAddressByState(String state);
}

Легко видеть, что определение метода репозитория с интерфейсом проекции почти такое же, как и с классом сущности.

Единственное отличие состоит в том, что в качестве типа элемента в возвращаемой коллекции используется интерфейс проекции, а не класс сущности.

Давайте проведем быстрый тест проекции адреса :

@Autowired
private AddressRepository addressRepository;

@Test
public void whenUsingClosedProjections_thenViewWithRequiredPropertiesIsReturned() {
AddressView addressView = addressRepository.getAddressByState("CA").get(0);
assertThat(addressView.getZipCode()).isEqualTo("90001");
// ...
}

За кулисами Spring создает прокси-экземпляр интерфейса проекции для каждого объекта сущности, и все вызовы прокси перенаправляются на этот объект.

Мы можем использовать проекции рекурсивно. Например, вот интерфейс проекции для класса Person :

public interface PersonView {
String getFirstName();

String getLastName();
}

Теперь добавим метод с возвращаемым типом PersonView, вложенной проекцией, в проекцию Address :

public interface AddressView {
// ...
PersonView getPerson();
}

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

Мы проверим вложенные проекции, добавив несколько операторов в только что написанный метод тестирования:

// ...
PersonView personView = addressView.getPerson();
assertThat(personView.getFirstName()).isEqualTo("John");
assertThat(personView.getLastName()).isEqualTo("Doe");

Обратите внимание, что рекурсивные проекции работают только в том случае, если мы переходим от стороны-владельца к обратной стороне. Если мы сделаем это наоборот, вложенная проекция будет иметь значение null .

3.2. Открытые проекции

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

Есть еще один вид проекции на основе интерфейса, открытые проекции. Эти проекции позволяют нам определять методы интерфейса с непревзойденными именами и возвращаемыми значениями, вычисляемыми во время выполнения.

Вернемся к интерфейсу проекции Person и добавим новый метод:

public interface PersonView {
// ...

@Value("#{target.firstName + ' ' + target.lastName}")
String getFullName();
}

Аргументом аннотации @Value является выражение SpEL, в котором целевое обозначение указывает на вспомогательный объект сущности.

Теперь мы определим другой интерфейс репозитория:

public interface PersonRepository extends Repository<Person, Long> {
PersonView findByLastName(String lastName);
}

Для простоты мы вернем только один проекционный объект вместо коллекции.

Этот тест подтверждает, что открытые прогнозы работают должным образом:

@Autowired
private PersonRepository personRepository;

@Test
public void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned() {
PersonView personView = personRepository.findByLastName("Doe");

assertThat(personView.getFullName()).isEqualTo("John Doe");
}

Однако у открытых проекций есть недостаток; Spring Data не может оптимизировать выполнение запросов, так как заранее не знает, какие свойства будут использоваться. Таким образом, мы должны использовать открытые проекции только тогда, когда закрытые проекции не могут удовлетворить наши требования.

4. Классовые прогнозы

Вместо использования прокси-серверов, которые Spring Data создает из интерфейсов проекций, мы можем определить свои собственные классы проекций.

Например, вот класс проекции для сущности Person :

public class PersonDto {
private String firstName;
private String lastName;

public PersonDto(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

// getters, equals and hashCode
}

Чтобы класс проекции работал в тандеме с интерфейсом репозитория, имена параметров его конструктора должны совпадать со свойствами корневого класса сущностей.

Мы также должны определить реализации equals и hashCode ; они позволяют Spring Data обрабатывать проекционные объекты в коллекции.

Теперь добавим метод в репозиторий Person :

public interface PersonRepository extends Repository<Person, Long> {
// ...

PersonDto findByFirstName(String firstName);
}

Этот тест проверяет нашу проекцию на основе классов:

@Test
public void whenUsingClassBasedProjections_thenDtoWithRequiredPropertiesIsReturned() {
PersonDto personDto = personRepository.findByFirstName("John");

assertThat(personDto.getFirstName()).isEqualTo("John");
assertThat(personDto.getLastName()).isEqualTo("Doe");
}

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

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

Класс сущностей может иметь множество проекций. В некоторых случаях мы можем использовать определенный тип, но в других случаях нам может понадобиться другой тип. Иногда нам также нужно использовать сам класс сущности.

Определение отдельных интерфейсов или методов репозитория только для поддержки нескольких типов возвращаемых данных является громоздким. Чтобы справиться с этой проблемой, Spring Data предлагает лучшее решение — динамические проекции.

Мы можем применить динамические проекции, просто объявив метод репозитория с параметром класса :

public interface PersonRepository extends Repository<Person, Long> {
// ...

<T> T findByLastName(String lastName, Class<T> type);
}

Передав в такой метод тип проекции или класс сущности, мы можем получить объект нужного типа:

@Test
public void whenUsingDynamicProjections_thenObjectWithRequiredPropertiesIsReturned() {
Person person = personRepository.findByLastName("Doe", Person.class);
PersonView personView = personRepository.findByLastName("Doe", PersonView.class);
PersonDto personDto = personRepository.findByLastName("Doe", PersonDto.class);

assertThat(person.getFirstName()).isEqualTo("John");
assertThat(personView.getFirstName()).isEqualTo("John");
assertThat(personDto.getFirstName()).isEqualTo("John");
}

6. Заключение

В этой статье мы обсудили различные типы проекций Spring Data JPA.

Исходный код этой статьи доступен на GitHub . Это проект Maven, и он должен работать как есть.