Spring Data JPA-Projektionen

1. Übersicht

Wenn Sie Spring Data JPA zum Implementieren der Persistenzschicht verwenden, gibt das Repository normalerweise eine oder mehrere Instanzen der Stammklasse zurück. Meistens benötigen wir jedoch nicht alle Eigenschaften der zurückgegebenen Objekte.

In solchen Fällen kann es wünschenswert sein, Daten als Objekte angepasster Typen abzurufen. Diese Typen spiegeln Teilansichten der Stammklasse wider und enthalten nur Eigenschaften, die uns wichtig sind. Hier bieten sich Projektionen an.

2. Ersteinrichtung

Der erste Schritt besteht darin, das Projekt einzurichten und die Datenbank zu füllen.

2.1. Maven-Abhängigkeiten

Informationen zu Abhängigkeiten finden Sie in Abschnitt 2 dieses Lernprogramms.

2.2. Entitätsklassen

Definieren wir zwei Entitätsklassen:

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

Und:

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

Die Beziehung zwischen Personen- und Adressentitäten ist bidirektional eins zu eins: Adresse ist die besitzende Seite und Person ist die umgekehrte Seite.

Beachten Sie, dass wir in diesem Tutorial eine eingebettete Datenbank verwenden - H2.

Wenn eine eingebettete Datenbank konfiguriert ist, generiert Spring Boot automatisch zugrunde liegende Tabellen für die von uns definierten Entitäten.

2.3. SQL-Skripte

Wir verwenden das Skript projection-insert-data.sql , um beide Backing-Tabellen zu füllen :

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

Um die Datenbank nach jedem Testlauf zu bereinigen, können wir ein anderes Skript mit dem Namen projection-clean-up-data.sql verwenden :

DELETE FROM address; DELETE FROM person;

2.4. Testklasse

Um zu bestätigen, dass Projektionen korrekte Daten liefern, benötigen wir eine Testklasse:

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

Mit den angegebenen Anmerkungen erstellt Spring Boot die Datenbank, fügt Abhängigkeiten ein und füllt und bereinigt Tabellen vor und nach der Ausführung jeder Testmethode.

3. Schnittstellenbasierte Projektionen

Bei der Projektion einer Entität ist es selbstverständlich, sich auf eine Schnittstelle zu verlassen, da keine Implementierung bereitgestellt werden muss.

3.1. Geschlossene Projektionen

Wenn wir auf die Adressklasse zurückblicken , sehen wir, dass sie viele Eigenschaften hat, aber nicht alle sind hilfreich. Zum Beispiel reicht manchmal eine Postleitzahl aus, um eine Adresse anzugeben.

Deklarieren wir eine Projektionsschnittstelle für die Adressklasse :

public interface AddressView { String getZipCode(); }

Verwenden Sie es dann in einer Repository-Schnittstelle:

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

Es ist leicht zu erkennen, dass das Definieren einer Repository-Methode mit einer Projektionsschnittstelle fast dasselbe ist wie mit einer Entitätsklasse.

Der einzige Unterschied besteht darin, dass die Projektionsschnittstelle anstelle der Entitätsklasse als Elementtyp in der zurückgegebenen Auflistung verwendet wird.

Lassen Sie uns einen kurzen Test der Adressprojektion durchführen :

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

Hinter den Kulissen erstellt Spring für jedes Entitätsobjekt eine Proxy-Instanz der Projektionsschnittstelle, und alle Aufrufe an den Proxy werden an dieses Objekt weitergeleitet.

Wir können Projektionen rekursiv verwenden. Hier ist zum Beispiel eine Projektionsschnittstelle für die Person- Klasse:

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

Nun fügen wir eine Methode mit dem Rückgabetyp PersonView - eine verschachtelte Projektion - in der Adress Projektion:

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

Beachten Sie, dass die Methode, die die verschachtelte Projektion zurückgibt, denselben Namen haben muss wie die Methode in der Stammklasse, die die zugehörige Entität zurückgibt.

Lassen Sie uns verschachtelte Projektionen überprüfen, indem wir der soeben geschriebenen Testmethode einige Anweisungen hinzufügen:

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

Beachten Sie, dass rekursive Projektionen nur funktionieren, wenn wir von der besitzenden Seite zur inversen Seite wechseln. Wenn wir es umgekehrt machen würden, würde die verschachtelte Projektion auf null gesetzt .

3.2. Projektionen öffnen

Bis zu diesem Punkt haben wir geschlossene Projektionen durchlaufen, die Projektionsschnittstellen angeben, deren Methoden genau mit den Namen der Entitätseigenschaften übereinstimmen.

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.

Wir können dynamische Projektionen anwenden, indem wir eine Repository-Methode mit einem Class- Parameter deklarieren :

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

Durch Übergeben eines Projektionstyps oder der Entitätsklasse an eine solche Methode können wir ein Objekt des gewünschten Typs abrufen:

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

In diesem Artikel haben wir verschiedene Arten von Spring Data JPA-Projektionen behandelt.

Der Quellcode für dieses Tutorial ist auf GitHub verfügbar. Dies ist ein Maven-Projekt und sollte unverändert ausgeführt werden können.