Projeções JPA de dados da primavera
1. Visão geral
Ao usarSpring Data JPA para implementar a camada de persistência, o repositório normalmente retorna uma ou mais instâncias da classe raiz. No entanto, na maioria das vezes, não precisamos de todas as propriedades dos objetos retornados.
Nesses casos, pode ser desejável recuperar dados como objetos de tipos personalizados. These types reflect partial views of the root class, containing only properties we care about. É aqui que as projeções são úteis.
2. Configuração inicial
O primeiro passo é configurar o projeto e preencher o banco de dados.
2.1. Dependências do Maven
Para dependências, verifique a seção 2 dethis tutorial.
2.2. Classes de Entidade
Vamos definir duas classes de entidade:
@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
}
And:
@Entity
public class Person {
@Id
private Long id;
private String firstName;
private String lastName;
@OneToOne(mappedBy = "person")
private Address address;
// getters and setters
}
O relacionamento entre as entidadesPerson eAddress é bidirecional um para um:Address é o lado proprietário ePerson é o lado inverso.
Observe neste tutorial que usamos um banco de dados incorporado - H2.
Quando um banco de dados embutido é configurado, Spring Boot gera automaticamente tabelas subjacentes para as entidades que definimos.
2.3. Scripts SQL
Usamos o scriptprojection-insert-data.sql para preencher ambas as tabelas de apoio:
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');
Para limpar o banco de dados após cada execução de teste, podemos usar outro script, denominadoprojection-clean-up-data.sql:
DELETE FROM address;
DELETE FROM person;
2.4. Classe de teste
Para confirmar que as projeções produzem dados corretos, precisamos de uma classe de teste:
@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
}
Com as anotações fornecidas,Spring Boot creates the database, injects dependencies, and populates and cleans up tables before and after each test method’s execution.
3. Projeções baseadas em interface
Ao projetar uma entidade, é natural contar com uma interface, pois não precisaremos fornecer uma implementação.
3.1. Projeções fechadas
Olhando novamente para a classeAddress, podemos verit has many properties, yet not all of them are helpful.. Por exemplo, às vezes um código postal é suficiente para indicar um endereço.
Vamos declarar uma interface de projeção para a classeAddress:
public interface AddressView {
String getZipCode();
}
Em seguida, use-o em uma interface de repositório:
public interface AddressRepository extends Repository {
List getAddressByState(String state);
}
É fácil ver que definir um método de repositório com uma interface de projeção é praticamente o mesmo que definir um método de entidade.
A única diferença é quethe projection interface, rather than the entity class, is used as the element type in the returned collection.
Vamos fazer um teste rápido da projeção deAddress:
@Autowired
private AddressRepository addressRepository;
@Test
public void whenUsingClosedProjections_thenViewWithRequiredPropertiesIsReturned() {
AddressView addressView = addressRepository.getAddressByState("CA").get(0);
assertThat(addressView.getZipCode()).isEqualTo("90001");
// ...
}
Nos bastidores,Spring creates a proxy instance of the projection interface for each entity object, and all calls to the proxy are forwarded to that object.
Podemos usar projeções recursivamente. Por exemplo, aqui está uma interface de projeção para a classePerson:
public interface PersonView {
String getFirstName();
String getLastName();
}
Agora, vamos adicionar um método com o tipo de retornoPersonView - uma projeção aninhada - na projeçãoAddress:
public interface AddressView {
// ...
PersonView getPerson();
}
Observe que o método que retorna a projeção aninhada deve ter o mesmo nome que o método na classe raiz que retorna a entidade relacionada.
Vamos verificar as projeções aninhadas adicionando algumas instruções ao método de teste que acabamos de escrever:
// ...
PersonView personView = addressView.getPerson();
assertThat(personView.getFirstName()).isEqualTo("John");
assertThat(personView.getLastName()).isEqualTo("Doe");
Observe querecursive projections only work if we traverse from the owning side to the inverse side. Se fizéssemos o contrário, a projeção aninhada seria definida comonull.
3.2. Projeções Abertas
Até este ponto, passamos por projeções fechadas, que indicam interfaces de projeção cujos métodos correspondem exatamente aos nomes das propriedades da entidade.
Há outro tipo de projeções baseadas em interface: projeções abertas. These projections enable us to define interface methods with unmatched names and with return values computed at runtime.
Vamos voltar para a interface de projeçãoPerson e adicionar um novo método:
public interface PersonView {
// ...
@Value("#{target.firstName + ' ' + target.lastName}")
String getFullName();
}
O argumento para a anotação@Value é uma expressão SpEL, na qual o designadortarget indica o objeto de entidade de apoio.
Agora, vamos definir outra interface de repositório:
public interface PersonRepository extends Repository {
PersonView findByLastName(String lastName);
}
Para simplificar, retornamos apenas um único objeto de projeção em vez de uma coleção.
Este teste confirma que as projeções abertas funcionam como esperado:
@Autowired
private PersonRepository personRepository;
@Testpublic void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned() {
PersonView personView = personRepository.findByLastName("Doe");
assertThat(personView.getFullName()).isEqualTo("John Doe");
}
As projeções abertas têm uma desvantagem: o Spring Data não pode otimizar a execução da consulta, pois não sabe com antecedência quais propriedades serão usadas. Assim,we should only use open projections when closed projections aren’t capable of handling our requirements.
4. Projeções baseadas em classe
Em vez de usar proxies que Spring Data cria para nós a partir de interfaces de projeção,we can define our own projection classes.
Por exemplo, aqui está uma classe de projeção para a entidadePerson:
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
}
Para que uma classe de projeção funcione em conjunto com uma interface de repositório, os nomes dos parâmetros de seu construtor devem corresponder às propriedades da classe de entidade raiz.
Devemos também definir as implementaçõesequalsehashCode - elas permitem que o Spring Data processe objetos de projeção em uma coleção.
Agora, vamos adicionar um método ao repositórioPerson:
public interface PersonRepository extends Repository {
// ...
PersonDto findByFirstName(String firstName);
}
Este teste verifica nossa projeção baseada em classe:
@Test
public void whenUsingClassBasedProjections_thenDtoWithRequiredPropertiesIsReturned() {
PersonDto personDto = personRepository.findByFirstName("John");
assertThat(personDto.getFirstName()).isEqualTo("John");
assertThat(personDto.getLastName()).isEqualTo("Doe");
}
Observe que, com a abordagem baseada em classe, não podemos usar projeções aninhadas.
5. Projeções Dinâmicas
Uma classe de entidade pode ter muitas projeções. Em alguns casos, podemos usar um determinado tipo, mas em outros casos, podemos precisar de outro tipo. Às vezes, também precisamos usar a própria classe de entidade.
Definir interfaces ou métodos separados de repositório apenas para suportar vários tipos de retorno é complicado. Para lidar com esse problema, o Spring Data fornece uma solução melhor: projeções dinâmicas.
Podemos aplicar projeções dinâmicas apenas declarando um método de repositório com um parâmetroClass:
public interface PersonRepository extends Repository {
// ...
T findByLastName(String lastName, Class type);
}
Ao passar um tipo de projeção ou a classe de entidade para esse método, podemos recuperar um objeto do tipo desejado:
@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. Conclusão
Neste artigo, analisamos vários tipos de projeções JPA do Spring Data.
O código-fonte deste tutorial está disponívelover on GitHub. Este é um projeto do Maven e deve poder ser executado como está.