Projections JPA Spring Data

1. Vue d'ensemble

Lorsque vous utilisez Spring Data JPA pour implémenter la couche de persistance, le référentiel renvoie généralement une ou plusieurs instances de la classe racine. Cependant, le plus souvent, nous n'avons pas besoin de toutes les propriétés des objets retournés.

Dans de tels cas, il peut être souhaitable de récupérer des données en tant qu'objets de types personnalisés. Ces types reflètent des vues partielles de la classe racine, contenant uniquement les propriétés qui nous intéressent. C'est là que les projections sont utiles.

2. Configuration initiale

La première étape consiste à configurer le projet et à remplir la base de données.

2.1. Dépendances de Maven

Pour les dépendances, veuillez consulter la section 2 de ce didacticiel.

2.2. Classes d'entité

Définissons deux classes d'entités:

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

Et:

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

La relation entre les entités Person et Address est bidirectionnelle un-à-un: l' adresse est le côté propriétaire et Person est le côté inverse.

Remarquez dans ce tutoriel, nous utilisons une base de données embarquée - H2.

Lorsqu'une base de données intégrée est configurée, Spring Boot génère automatiquement des tables sous-jacentes pour les entités que nous avons définies.

2.3. Scripts SQL

Nous utilisons le script projection-insert-data.sql pour remplir les deux tables de sauvegarde:

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

Pour nettoyer la base de données après chaque test, nous pouvons utiliser un autre script, nommé projection-clean-up-data.sql :

DELETE FROM address; DELETE FROM person;

2.4. Classe d'essai

Pour confirmer que les projections produisent des données correctes, nous avons besoin d'une classe de 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 }

Avec les annotations données, Spring Boot crée la base de données, injecte les dépendances et remplit et nettoie les tables avant et après l'exécution de chaque méthode de test.

3. Projections basées sur l'interface

Lors de la projection d'une entité, il est naturel de s'appuyer sur une interface, car nous n'avons pas besoin de fournir une implémentation.

3.1. Projections fermées

En regardant en arrière la classe Address , nous pouvons voir qu'elle possède de nombreuses propriétés, mais toutes ne sont pas utiles. Par exemple, parfois un code postal suffit pour indiquer une adresse.

Déclarons une interface de projection pour la classe Address :

public interface AddressView { String getZipCode(); }

Ensuite, utilisez-le dans une interface de référentiel:

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

Il est facile de voir que la définition d'une méthode de référentiel avec une interface de projection est à peu près la même chose qu'avec une classe d'entité.

La seule différence est que l'interface de projection, plutôt que la classe d'entité, est utilisée comme type d'élément dans la collection renvoyée.

Faisons un test rapide de la projection d' adresse :

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

Dans les coulisses, Spring crée une instance de proxy de l'interface de projection pour chaque objet d'entité, et tous les appels au proxy sont transférés vers cet objet.

Nous pouvons utiliser des projections de manière récursive. Par exemple, voici une interface de projection pour la classe Person :

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

Maintenant, ajoutons une méthode avec le type de retour PersonView - une projection imbriquée - dans la projection d' adresse :

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

Notez que la méthode qui renvoie la projection imbriquée doit avoir le même nom que la méthode de la classe racine qui renvoie l'entité associée.

Vérifions les projections imbriquées en ajoutant quelques instructions à la méthode de test que nous venons d'écrire:

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

Notez que les projections récursives ne fonctionnent que si nous passons du côté propriétaire au côté inverse. Si nous procédions à l'inverse, la projection imbriquée serait définie sur null .

3.2. Projections ouvertes

Jusqu'à présent, nous sommes passés par des projections fermées, qui indiquent des interfaces de projection dont les méthodes correspondent exactement aux noms des propriétés des entités.

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.

Nous pouvons appliquer des projections dynamiques simplement en déclarant une méthode de référentiel avec un paramètre Class :

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

En passant un type de projection ou la classe d'entité à une telle méthode, nous pouvons récupérer un objet du type souhaité:

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

Dans cet article, nous avons passé en revue différents types de projections Spring Data JPA.

Le code source de ce didacticiel est disponible à l'adresse over sur GitHub. Il s'agit d'un projet Maven et devrait pouvoir s'exécuter tel quel.