Рубрики
Без рубрики

Весенние данные Прогнозы JPA

Краткий и практический обзор прогнозов JPA по весенним данным.

Автор оригинала: Nguyen Nam Thai.

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 {
    List 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 – вложенная проекция – в Адрес проекцию:

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

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

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

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

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

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

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

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

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

public interface PersonView {
    // ...

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

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

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

public interface PersonRepository extends Repository {
    PersonView findByLastName(String lastName);
}

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

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

@Autowired
private PersonRepository personRepository;

@Testpublic void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned() {
    PersonView personView = personRepository.findByLastName("Doe");
 
    assertThat(personView.getFullName()).isEqualTo("John Doe");
}

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

4. Проекции на основе классов

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

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

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 {
    // ...

    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 {
    // ...

     T findByLastName(String lastName, Class 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. Заключение

В этой статье мы рассмотрели различные типы прогнозов JPA весенних данных.

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