Spring Data JPA-projecties

1. Overzicht

Wanneer Spring Data JPA wordt gebruikt om de persistentielaag te implementeren, retourneert de repository doorgaans een of meer instanties van de rootklasse. Vaker wel dan niet hebben we echter niet alle eigenschappen van de geretourneerde objecten nodig.

In dergelijke gevallen kan het wenselijk zijn om gegevens op te halen als objecten van aangepaste typen. Deze typen weerspiegelen gedeeltelijke weergaven van de hoofdklasse en bevatten alleen eigenschappen waar we om geven. Dit is waar projecties van pas komen.

2. Eerste installatie

De eerste stap is het opzetten van het project en het vullen van de database.

2.1. Afhankelijkheden van Maven

Raadpleeg sectie 2 van deze tutorial voor afhankelijkheden.

2.2. Entiteitsklassen

Laten we twee entiteitsklassen definiëren:

@Entity openbare klasse Adres {@Id privé Lange id; @OneToOne particulier persoon; private String staat; particuliere String stad; privé String Street; private String zipCode; // getters en setters}

En:

@Entity openbare klasse Persoon {@Id privé Lange id; private String voornaam; private String achternaam; @OneToOne (mappedBy = "person") privé adres; // getters en setters}

De relatie tussen Persoon en Adres entiteiten is bidirectioneel een-op-een: Adres is de kant van het bezit, en Persoon is de omgekeerde kant.

Merk op dat we in deze tutorial een ingesloten database gebruiken - H2.

Wanneer een embedded database is geconfigureerd, genereert Spring Boot automatisch onderliggende tabellen voor de entiteiten die we hebben gedefinieerd.

2.3. SQL-scripts

Wij gebruiken de projection-insert-data.sql script om beide achtergrondtabellen te vullen:

INVOEGEN IN persoon (id, voornaam, achternaam) WAARDEN (1, 'Jan', 'Doe'); INVOEGEN IN adres (id, person_id, staat, stad, straat, postcode) WAARDEN (1,1, 'CA', 'Los Angeles', 'Standford Ave', '90001');

Om de database na elke testrun op te schonen, kunnen we een ander script gebruiken, genaamd projection-clean-up-data.sql:

VERWIJDEREN VAN adres; VERWIJDEREN VAN persoon;

2.4. Test klasse

Om te bevestigen dat projecties correcte gegevens opleveren, hebben we een testklasse nodig:

@DataJpaTest @RunWith (SpringRunner.class) @Sql (scripts = "/projection-insert-data.sql") @Sql (scripts = "/projection-clean-up-data.sql", executionPhase = AFTER_TEST_METHOD) openbare klasse JpaProjectionIntegrationTest {// ingevoegde velden en testmethoden}

Met de gegeven annotaties, Spring Boot maakt de database, injecteert afhankelijkheden en vult tabellen in en ruimt ze op voor en na de uitvoering van elke testmethode.

3. Op interface gebaseerde projecties

Bij het projecteren van een entiteit is het normaal om op een interface te vertrouwen, omdat we geen implementatie hoeven te bieden.

3.1. Gesloten projecties

Terugkijkend op de Adres klasse, kunnen we zien het heeft veel eigenschappen, maar ze zijn niet allemaal nuttig. Soms is een postcode bijvoorbeeld voldoende om een ​​adres aan te geven.

Laten we een projectie-interface declareren voor het Adres klasse:

openbare interface AddressView {String getZipCode (); }

Gebruik het vervolgens in een repository-interface:

openbare interface AddressRepository breidt Repository {List getAddressByState (String-status) uit; }

Het is gemakkelijk in te zien dat het definiëren van een repository-methode met een projectie-interface vrijwel hetzelfde is als met een entiteitsklasse.

Het enige verschil is dat de projectie-interface, in plaats van de entiteitsklasse, wordt gebruikt als het elementtype in de geretourneerde verzameling.

Laten we de Adres projectie:

@Autowired privé AddressRepository addressRepository; @Test openbare leegte whenUsingClosedProjections_thenViewWithRequiredPropertiesIsReturned () {AddressView addressView = addressRepository.getAddressByState ("CA"). Get (0); assertThat (addressView.getZipCode ()). isEqualTo ("90001"); // ...}

Achter de schermen, Spring maakt een proxy-instantie van de projectie-interface voor elk entiteitsobject en alle oproepen naar de proxy worden doorgestuurd naar dat object.

We kunnen projecties recursief gebruiken. Hier is bijvoorbeeld een projectie-interface voor de Persoon klasse:

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

Laten we nu een methode toevoegen met het retourtype PersonView - een geneste projectie - in het Adres projectie:

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

Merk op dat de methode die de geneste projectie retourneert dezelfde naam moet hebben als de methode in de rootklasse die de gerelateerde entiteit retourneert.

Laten we geneste projecties verifiëren door een paar instructies toe te voegen aan de testmethode die we zojuist hebben geschreven:

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

Let daar op recursieve projecties werken alleen als we van de bezittende kant naar de inverse kant gaan. Als we het andersom zouden doen, zou de geneste projectie worden ingesteld op nul.

3.2. Open projecties

Tot nu toe hebben we gesloten projecties doorlopen, die projectie-interfaces aangeven waarvan de methoden exact overeenkomen met de namen van entiteitseigenschappen.

Er is een ander soort interface-gebaseerde projecties: open projecties. Deze projecties stellen ons in staat om interfacemethoden te definiëren met ongeëvenaarde namen en met geretourneerde waarden die tijdens runtime zijn berekend.

Laten we teruggaan naar de Persoon projectie-interface en voeg een nieuwe methode toe:

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

Het argument voor de @Waarde annotatie is een SpEL-expressie, waarin de doelwit aanduiding geeft het object van de ondersteunende entiteit aan.

Nu gaan we een andere repository-interface definiëren:

openbare interface PersonRepository breidt Repository {PersonView findByLastName (String lastName) uit; }

Om het eenvoudig te maken, retourneren we slechts één projectieobject in plaats van een verzameling.

Deze test bevestigt dat open projecties werken zoals verwacht:

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

Open projecties hebben een nadeel: Spring Data kan de uitvoering van query's niet optimaliseren omdat het van tevoren niet weet welke eigenschappen zullen worden gebruikt. Dus, we zouden alleen open projecties moeten gebruiken als gesloten projecties niet in staat zijn om aan onze vereisten te voldoen.

4. Op klassen gebaseerde projecties

In plaats van proxy's te gebruiken, creëert Spring Data voor ons op basis van projectie-interfaces, we kunnen onze eigen projectieklassen definiëren.

Hier is bijvoorbeeld een projectieklasse voor de Persoon entiteit:

openbare klasse PersonDto {private String firstName; private String achternaam; public PersonDto (String firstName, String lastName) {this.firstName = firstName; this.lastName = achternaam; } // getters, is gelijk aan en hashCode}

Om een ​​projectieklasse samen te laten werken met een repository-interface, moeten de parameternamen van de constructor overeenkomen met de eigenschappen van de hoofdentiteitsklasse.

We moeten ook definiëren is gelijk aan en hashCode implementaties - ze stellen Spring Data in staat projectieobjecten in een verzameling te verwerken.

Laten we nu een methode toevoegen aan het Persoon opslagplaats:

openbare interface PersonRepository breidt Repository {// ... PersonDto findByFirstName (String firstName) uit; }

Deze test verifieert onze op klassen gebaseerde projectie:

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

Merk op dat we met de op klassen gebaseerde benadering geen geneste projecties kunnen gebruiken.

5. Dynamische projecties

Een entiteitsklasse kan veel projecties hebben. In sommige gevallen kunnen we een bepaald type gebruiken, maar in andere gevallen hebben we mogelijk een ander type nodig. Soms moeten we ook de entiteitsklasse zelf gebruiken.

Het definiëren van afzonderlijke repository-interfaces of -methoden om alleen meerdere retourtypen te ondersteunen, is omslachtig. Om dit probleem aan te pakken, biedt Spring Data een betere oplossing: dynamische projecties.

We kunnen dynamische projecties toepassen door een repository-methode te declareren met een Klasse parameter:

openbare interface PersonRepository breidt Repository uit {// ... T findByLastName (String lastName, Class type); }

Door een projectietype of de entiteitsklasse door te geven aan een dergelijke methode, kunnen we een object van het gewenste type ophalen:

@Test openbare leegte 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. Conclusie

In dit artikel hebben we verschillende soorten Spring Data JPA-projecties besproken.

De broncode voor deze tutorial is beschikbaar op GitHub. Dit is een Maven-project en zou moeten kunnen draaien zoals het is.