Proiezioni JPA Spring Data

1. Panoramica

Quando si utilizza Spring Data JPA per implementare il livello di persistenza, il repository in genere restituisce una o più istanze della classe root. Tuttavia, il più delle volte, non abbiamo bisogno di tutte le proprietà degli oggetti restituiti.

In questi casi, potrebbe essere opportuno recuperare i dati come oggetti di tipi personalizzati. Questi tipi riflettono viste parziali della classe radice, contenenti solo le proprietà che ci interessano. È qui che le proiezioni tornano utili.

2. Configurazione iniziale

Il primo passo è impostare il progetto e popolare il database.

2.1. Dipendenze di Maven

Per le dipendenze, consulta la sezione 2 di questo tutorial.

2.2. Classi di entità

Definiamo due classi di entità:

@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 }

E:

@Entity public class Person { @Id private Long id; private String firstName; private String lastName; @OneToOne(mappedBy = "person") private Address address; // getters and setters }

La relazione tra le entità Persona e Indirizzo è bidirezionale uno a uno: l' indirizzo è il lato proprietario e Persona è il lato inverso.

Nota in questo tutorial, usiamo un database incorporato - H2.

Quando viene configurato un database incorporato, Spring Boot genera automaticamente le tabelle sottostanti per le entità che abbiamo definito.

2.3. Script SQL

Usiamo lo script projection-insert-data.sql per popolare entrambe le tabelle di supporto:

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');

Per ripulire il database dopo ogni esecuzione di test, possiamo utilizzare un altro script, denominato projection-clean-up-data.sql :

DELETE FROM address; DELETE FROM person;

2.4. Classe di prova

Per confermare che le proiezioni producono dati corretti, abbiamo bisogno di una classe di test:

@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 }

Con le annotazioni fornite, Spring Boot crea il database, inietta le dipendenze e popola e pulisce le tabelle prima e dopo l'esecuzione di ciascun metodo di test.

3. Proiezioni basate sull'interfaccia

Quando si proietta un'entità, è naturale fare affidamento su un'interfaccia, poiché non è necessario fornire un'implementazione.

3.1. Proiezioni chiuse

Guardando indietro alla classe Address , possiamo vedere che ha molte proprietà, ma non tutte sono utili. Ad esempio, a volte è sufficiente un codice postale per indicare un indirizzo.

Dichiariamo un'interfaccia di proiezione per la classe Address :

public interface AddressView { String getZipCode(); }

Quindi usalo in un'interfaccia di repository:

public interface AddressRepository extends Repository { List getAddressByState(String state); }

È facile vedere che la definizione di un metodo di repository con un'interfaccia di proiezione è praticamente la stessa di una classe di entità.

L'unica differenza è che l'interfaccia di proiezione, anziché la classe di entità, viene utilizzata come tipo di elemento nella raccolta restituita.

Facciamo un rapido test della proiezione dell'indirizzo :

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

Dietro le quinte, Spring crea un'istanza proxy dell'interfaccia di proiezione per ogni oggetto entità e tutte le chiamate al proxy vengono inoltrate a quell'oggetto.

Possiamo usare le proiezioni in modo ricorsivo. Ad esempio, ecco un'interfaccia di proiezione per la classe Person :

public interface PersonView { String getFirstName(); String getLastName(); }

Ora, aggiungiamo un metodo con il tipo di ritorno PersonView - una proiezione annidata - nella proiezione dell'indirizzo :

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

Si noti che il metodo che restituisce la proiezione annidata deve avere lo stesso nome del metodo nella classe radice che restituisce l'entità correlata.

Verifichiamo le proiezioni annidate aggiungendo alcune istruzioni al metodo di test che abbiamo appena scritto:

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

Notare che le proiezioni ricorsive funzionano solo se si passa dal lato proprietario al lato inverso. Se dovessimo fare il contrario, la proiezione annidata sarebbe impostata su null .

3.2. Proiezioni aperte

Fino a questo punto, abbiamo utilizzato proiezioni chiuse, che indicano interfacce di proiezione i cui metodi corrispondono esattamente ai nomi delle proprietà dell'entità.

There's another sort of interface-based projections: open projections. These projections enable us to define interface methods with unmatched names and with return values computed at runtime.

Let's go back to the Person projection interface and add a new method:

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

The argument to the @Value annotation is a SpEL expression, in which the target designator indicates the backing entity object.

Now, we'll define another repository interface:

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

To make it simple, we only return a single projection object instead of a collection.

This test confirms open projections work as expected:

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

Open projections have a drawback: Spring Data cannot optimize query execution as it doesn't know in advance which properties will be used. Thus, we should only use open projections when closed projections aren't capable of handling our requirements.

4. Class-Based Projections

Instead of using proxies Spring Data creates for us from projection interfaces, we can define our own projection classes.

For example, here's a projection class for the 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 }

For a projection class to work in tandem with a repository interface, the parameter names of its constructor must match properties of the root entity class.

We must also define equals and hashCode implementations – they allow Spring Data to process projection objects in a collection.

Now, let's add a method to the Person repository:

public interface PersonRepository extends Repository { // ... PersonDto findByFirstName(String firstName); }

This test verifies our class-based projection:

@Test public void whenUsingClassBasedProjections_thenDtoWithRequiredPropertiesIsReturned() { PersonDto personDto = personRepository.findByFirstName("John"); assertThat(personDto.getFirstName()).isEqualTo("John"); assertThat(personDto.getLastName()).isEqualTo("Doe"); }

Notice with the class-based approach, we cannot use nested projections.

5. Dynamic Projections

An entity class may have many projections. In some cases, we may use a certain type, but in other cases, we may need another type. Sometimes, we also need to use the entity class itself.

Defining separate repository interfaces or methods just to support multiple return types is cumbersome. To deal with this problem, Spring Data provides a better solution: dynamic projections.

Possiamo applicare proiezioni dinamiche semplicemente dichiarando un metodo di repository con un parametro Class :

public interface PersonRepository extends Repository { // ...  T findByLastName(String lastName, Class type); }

Passando un tipo di proiezione o la classe di entità a tale metodo, possiamo recuperare un oggetto del tipo desiderato:

@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. Conclusione

In questo articolo, abbiamo esaminato vari tipi di proiezioni JPA Spring Data.

Il codice sorgente di questo tutorial è disponibile su GitHub. Questo è un progetto Maven e dovrebbe essere in grado di funzionare così com'è.