Rest mit Spring Data JPA Projections

“Simplicity, carried to the extreme, becomes elegance.”

Jon Franklin

Die Entwicklung von Rest Schnittstellen mit Spring Boot wird durch viele Features des Frameworks begünstigt. Insbesondere durch die einfache Umsetzung der Rest Controller mit Hilfe einer Vielzahl von Annotationen und die Persistenz mit Spring Data JPA.

Während die Persistenzschicht mit ihren Repositories auf Entity Klassen arbeitet, werden vom Rest Controller DTOs verwenden. Entities und DTO arbeiten auf der Grundlagen identischer Domänenobjekte, unterscheiden sich aber zwangsläufig durch ihre sehr unterschiedliche Nutzung. So besitzen Entities häufig Attribute zur Speicherung des Änderungsdatums und eines eindeutigen Datenbankschlüssels.

Damit Rest Controller und Persistenzschicht miteinander arbeiten können, werden häufig Mapper genutzt, die zwischen den Darstellungsarten DTO und Entity konvertieren können.

@GetMapping("/{lastname})
public List<Person> list(@PathVariable String lastname) {
  return service.list(lastname).stream().map(mapper::map).collect(Collectors.toList());
} 

Durch die architektonische Trennung von Rest Controller und Persistenzschicht erhält man häufig Code wie den obigen. Der Service liefert eine Liste von Entities, die von einen Mapper ,in die DTOs der Wahl, konvertiert wird.

Das Erzeugen einer, von der Persistenzschicht verwalteten, Entity bedeutet aber immer einen zusätzlichen Overhead. Wenn die Entity dann im nächsten Augenblick wieder zerstört wird, stellt sich die berechtigte Frage, ob dies denn so sein muss.

Erfreulicherweise liefert Spring Data JPA mit Class-based Projections genau das richtige Werkzeug. Projektionen erlauben es in Spring Boot JPA statt der Entität der Repository Definition anderer Interfaces oder Klassen als Rückgabewert zu definieren. Diese werden dann mit den entsprechenden Werten aus der Anfrage bestückt und zurückgeliefert. Im Falle unserer DTO Instanzen benötigen wir dafür nur einen Konstruktor, der die gewünschten Attribute als Parameter definiert.

interface PersonRepository extends Repository<Person, Long> {
  List<PersonDto> findByLastname(String lastname);
}

Das obige Beispiel definiert ein Repository, dass mit der Methode findByLastname eine Liste von Personen aus der Ahnen Datenbank zurückliefert. Der zu Beginn gezeigte Endpoint reduziert sich mit dieser Repository Definition auf die folgende Variante.

@GetMapping("/{lastname})
public List<Person> list(@PathVariable String lastname) {
  return service.list(lastname);
} 

Der Mapper wird nicht mehr benötigt, weil schon das Repository die gewünschten DTO Instanzen liefert.

Hin und wieder benötigt aber auch die Businesslogik eine Liste von Personen aus der Datenbank. Dort werden in der Regel Entity Instanzen benötigt. Die Lösung dafür lautet Dynamic Projections und wird über generische Methoden gelöst.

interface PersonRepository extends Repository<Person, Long> {
  <T> List<T> findByLastname(String lastname, Class<T> type);
}

Die Query Methode erhält einen zusätzlichen Parameter, der die Klasse für den generischen Typ übergibt. Der Aufruf der Query Methode für PersonDto Instanzen für den Rest Controller ändert sich damit auf:

List<PersonDto> personDtos = personRepository.findByLastname("Kaiser", PersonDto.class);

Der Aufruf innerhalb der Businesslogik lautet dann:

List<Person> persons = personRepository.findByLastname("Kaiser", Person.class);

Ein weiterer Vorteil der Projections ist die Nutzung unterschiedlichster DTO Typen auf einem Repository ohne eine größere Zahl spezifischer Mapper zu erstellen. So kann z.B. neben der PersonDto Klasse auch eine DsgvoPersonDto Klasse genutzt werden, die keine DSGVO relevanten Daten enthält.

Wer seine Anwendung flinker und einfacher gestalten möchte und die Möglichkeit besitzt, die eigene Architektur zu ändern, kann mit den Projections von Spring Boot JPA eine Menge unnötigen Mapper Code sparen.