Domain Driven Design Repositories mit Spring Data

29.07.2021

Repositories sind einer der Bestandteile im Domain Driven Design. Mit Spring Data lassen sich Repositories für den Datenbankzugriff einfach und ohne Boilerplate-Code umsetzen. Aber wie implementiert man das insbesondere hinsichtlich Trennung des Domain Models von der Infrastruktur?

DDD-Repository mit Spring Data – ein Beispiel

Ein Hinweis noch vorab: Kenntnisse zu Domain Driven Design und Spring Data werden für diesen Artikel vorausgesetzt.

 

Im Domain Driven Design definiert man Repositories für Datenbankzugriffe innerhalb des Domain Models. Und zwar in der Regel anhand von Interfaces, da die Implementierungen abhängig von der Infrastruktur sind (d.h. von der verwendeten Technologie für den Datenbankzugriff), und daher in der Infrastrukturschicht, außerhalb des Domain Models, verortet sein sollten.

 

Schichtenarchitektur
Quelle: Eigene Darstellung

Das Repository-Interface gehört also in den gelben, die Implementierung in den blauen Bereich der im Diagramm dargestellten Schichtenarchitektur. Aber halt – in Spring Data definieren wir ja nur Interfaces. Die Implementierung schreiben wir gar nicht selbst; diese wird von Spring Data automatisch generiert!

Also könnten wir das Spring-Data-Interface ja direkt im Domain Model definieren, oder?

 

 

Das sähe dann so aus (man beachte das domain.model“ im Packagenamen):

package de.doubleslash.myapp.item.domain.model;

import org.springframework.data.repository.CrudRepository;

public interface ItemRepository extends CrudRepository<Item, Long> {

}

Trennung von  Domain Model und Infrastruktur

Mit dem Import des CrudRepository und der Ableitung davon ist allerdings nun ein Detail der Infrastruktur ins Domänenmodell gesickert.

Das wollen wir nicht – also passen wir das Interface an und entfernen die Spring-Data-spezifischen Anteile:

package de.doubleslash.myapp.item.domain.model;

public interface ItemRepository {

}

So bringt uns das Interface allerdings nicht mehr viel. Die erste Variante wäre schon nutzbar gewesen, da CrudRepository bereits eine Reihe an Methoden mitbringt. Unser neues Interface leitet allerdings nicht mehr davon ab und besitzt daher auch keine Methoden mehr.

Daher müssen wir die benötigten Methoden nun hier definieren:

package de.doubleslash.myapp.item.domain.model;

import java.util.List;

public interface ItemRepository {

    Item save(Item anItem);

    List<Item> findAll();

    List<Item> findByName(String name);

}

Nun definieren wir das Spring-Data-Interface in der Infrastrukturschicht. Gemäß des Ports-And-Adapters-Architekturstils, einer Ausprägung des oben illustrierten Schichtenmodells, kommt das Interface ins Package .port.adapter.persistence“:. Ich nenne das Interface JdbcItemRepository“, weil ich für das Beispiel Spring Data JDBC verwendet habe:

package de.doubleslash.myapp.item.port.adapter.persistence;

import org.springframework.data.repository.CrudRepository;

import de.doubleslash.myapp.item.domain.model.Item;
import de.doubleslash.myapp.item.domain.model.ItemRepository;

public interface JdbcItemRepository extends ItemRepository, CrudRepository<Item, Long> {

}

Der Clou an der Sache: das Interface erbt sowohl von unserem ItemRepository aus dem Domain Model, als auch von Spring Datas CrudRepository! So findet Spring Data auch die Methoden, die nur in unserem ItemRepository definiert sind, nicht aber in CrudRepository – wie im obigen Beispiel die Methode findByName(…) – und generiert entsprechende Implementierungen für uns.

Lediglich Methoden mit einer @Query-Annotation müssen in beiden Interfaces definiert werden. Einmal mit den Annotationen, und zwar wegen Bezug zur Infrastruktur in JdbcItemRepository, und dann noch einmal im ItemRepository, damit die Methode darüber aufgerufen werden kann. Dort dann natürlich ohne Annotationen.

package de.doubleslash.myapp.item.domain.model;

import java.util.List;

public interface ItemRepository {

    // restliche Methoden

    List<Item> findByCustomQuery(String name);

}
package de.doubleslash.myapp.item.port.adapter.persistence;

import java.util.List;

import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

import de.doubleslash.myapp.item.domain.model.Item;
import de.doubleslash.myapp.item.domain.model.ItemRepository;

public interface JdbcItemRepository extends ItemRepository, CrudRepository<Item, Long> {

    @Override
    @Query("select * from Item i where i.name = :name")
    List<Item> findByCustomQuery(@Param("name") String name);

}

In der Projektstruktur sieht das Ganze dann wie folgt aus:

Quelle: Eigene Darstellung

Die Packagestruktur ist übrigens den Beispielen aus dem Buch „Implementing Domain Driven Design“ von Vaughn Vernon nachempfunden.

 

Fazit

Mit Spring Data lassen sich Repositories sehr einfach umsetzen. Und mit einem kleinen Kniff lässt sich auch eine saubere Schichtenarchitektur mit Trennung von Domänen- und Infrastrukturschicht erreichen.

Der Quellcode für das Demoprojekt ist auf GitHub veröffentlicht:
https://github.com/swa-ds/ddd-repo-demo

 

Mehr zu Softwareentwicklung erfahren 

Zurück zur Übersicht

Ein Kommentar zur “Domain Driven Design Repositories mit Spring Data

  1. Hallo,
    sehr schön dieses Beispiel, aber eines interessiert mich noch. Wie funktioniert die Spring Magic bzgl. der Klasse ‚Item‘?
    Normalerweise würde ich doch eine mit @Entity notierte Klasse in der Persistenz-Schicht erstellen und und dieses dann auf die o.g. Klasse ‚Item‘ mappen.
    Das scheint hier doch transparent zu geschehen. Sehe ich das falsch oder verstehe ich nur die Spring Magic nicht.
    Vielen Dank und Grüße

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*