Anti-Patterns und häufige Fehler bei "DDD mit Spring JPA"
Im ST2-Praktikum begegnen uns die nachfolgenden Anti-Patterns und Fehler häufig. Will man eine DDD-Architektur mit Spring JPA umsetzen, dann sollte man diese vermeiden.
- Video(s) hierzu
-
Wenn Sie nicht so genau wissen, was mit “Anti-Patterns” gemeint ist, dann schauen Sie sich
das oben genannte Video dazu an. Die nachfolgende Liste kann Ihnen helfen, oft auftretende Fehler zu vermeiden.
Anti-Patterns
Die nachfolgenden Patterns führen nicht zu Fehlern im Code, aber zu schlechtem Code.
UUIDs statt Entity-Referenzen
Einige Male haben wir folgenden Code gesehen:
@Entity
public class Planet {
@Id
@GeneratedValue
private UUID id;
private UUID northNeighbourId;
private UUID eastNeighbourId;
private UUID southNeighbourId;
private UUID westNeighbourId;
//...
}
Nur weil das Certification-Interface UUIDs verwendet, ist das noch lange kein Grund, das im Entity auch zu machen!
So eine Programmierung führt dazu, dass man im ApplicationService dann so in der Art programmieren muss (Pseudocode -
habe es nicht auf Lauffähigkeit getestet):
public Planet findPlanet( UUID id ) {
Planet planet = planetRepository.findById( id ).orElseThrow( () -> new PlanetException( "..." ) );
UUID northNeighbourId = planet.northNeighbourId();
Planet northNeighbour = planetRepository.findById( northNeighbourId ).
orElseThrow( () -> new PlanetException( "..." ) );
// etc.
}
Statt dessen machen Sie IMMER Beziehungen zu anderen Entities:
@Entity
public class Planet {
@Id
@GeneratedValue
private UUID id;
@OneToOne
private Planet northNeighbour;
@OneToOne
private Planet eastNeighbour;
@OneToOne
private Planet southNeighbour;
@OneToOne
private Planet westNeighbour;
//...
}
Wenn Sie das machen, dann werden beim Fetch des Entities aus der DB die Nachbarschaftsbeziehungen mit geholt.
Müssen Sie sich also gar nicht drum kümmern! Sie können in den Methoden von Planet
direkt auf die Nachbarn zugreifen.
Code Copy-Pasting
Häufig auch das Copy-Pasting von Code, der eigentlich identisch ist. Vielleicht so in der Art:
public void setNorthNeighbourRelation( Planet planet ) {
//
//
// ... irgendwelche 20 Zeilen von Prüfungen und setzen der Beziehung ...
//
}
public void setEastNeighbourRelation( Planet planet ) {
//
//
// ... derselbe Code, nur "north" durch "east" ersetzt ...
//
}
// ... und dann nochmal genauso für "south" und "west"
Es leuchtet vermutlich ein, warum das ein ganz schlechter Stil ist: Wenn Sie einen Bug einbauen und den dann
fixen müssen, dann müssen Sie ihn 4x fixen. Wie hoch ist die Chance, dass man es 3x macht und 1x vergisst?
Prozedurales statt objekt-orientiertes Programmieren
Häufiger konnte man auch folgendes Anti-Pattern, das im Kern ein prozedurales statt objekt-orientiertes Programmieren
ist. Im ApplicationService von Robot
gab es dann so in etwa die folgenden Methoden:
public void move( Direction direction, Robot robot ) {
// ... und dann wurde der Robot mittels Repo aus der DB geholt, auf einen neuen Planeten bewegt,
// und dann mittels Repo wieder abgespeichert.
}
Damit ist die Business-Logik für den Robot
da, wo sie nicht hingehört: nämlich außerhalb des Robots
.
Viel besser (und objektorientiert …) ist es, diese Logik im Robot
selbst zu implementieren. Ganz grob etwa so:
@Service
public class RobotApplicationService {
//...
public void executeCommand( Command command ) {
//...
Robot robot = robotRepository.findById( command.getRobotId() ).
orElseThrow( () -> new RobotException( "..." );
if ( command.isMove() ) {
robot.move( command.getDirection() );
}
}
//...
}
@Entity
public class Robot {
//...
public void move( Direction direction ) {
// Move-Logik ist jetzt da, wo sie hingehört: INNERHALB des Robots.
//...
}
//...
}
Business-Logik im ApplicationService, statt in der Domain (also in den Entities)
Wenn man das vorige Anti-Pattern “konsequent umsetzt” (Achtung, Ironie!), dann ist am Ende die gesamte Business-Logik
in einer Klasse, nämlich dem ApplicationService. Die Entities sind dann nur noch reine “CRUD-Container”. Damit hat
man jetzt Art von Reuse und guter Wartbarkeit wirkungsvoll verhindert :-(.
Value Objects mit public Settern
Thematischer Schwenk: Value Objects (… @Embeddable
-Klassen …) sind per Definition immutable. Sie haben also
keine public Setter! Sonst kann man sie von außen ändern.
Value Objects ohne passende equals( Object obj )
Implementation
Zwei Value Objects sind per Definition gleich, wenn ihre Attributwerte gleich sind. Daraus folgt zwingend, dass
sie eine Implementation für die Methode equals( Object obj )
haben, die alle Attributwerte einbezieht und
vergleicht. Wenn man das nicht implementiert, wird nur die Java-Object-ID verglichen - und dann sind zwei
verschiedene Instanzen immer ungleich.
Repositories in den Entities selbst
Man könnte versucht sein, im Entity selbst ein Repository als Membervariable (oder Methodenparameter) zu haben -
dann könnte man ja im Entity direkt speichern, oder andere Entities holen.
Das sollte man aber vermeiden, denn der Domain Layer ist technologie-agnostisch. Man bräuchte ja keine relationale
Datenbank zu nutzen, sondern könnte Objekte anders persistieren. Man sollte also das Lifecycle-Management
einer DB außerhalb des Domain Layers machen - z.B. in einem ApplicationService.
IDs vom Typ Long statt UUID
In ganz vielen Web-Tutorials zu JPA findet man folgende Implementation einer Entity-ID:
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
Long
-IDs haben aber große Nachteile, insbesondere dass sie Sequenzen bilden und damit “erratbar” werden. Daher:
Nehmen Sie im Regelfall immer UUIDs als Entity-ID. Also besser:
@Id
@GeneratedValue
private UUID id;
System.out.println
statt Verwendung des Loggers
System.out.println
ist praktisch, um sich ohne Debugger den “Trace” einer Anwendung ausgeben zu lassen.
Für das Praktikum ist das egal, aber in der Praxis ist es immer besser, stattdessen einen Logger zu benutzen.
Der funktioniert auch im Produktiv-Environment (im Gegensatz zu System.out.println). In Spring ist das ganz einfach:
@Entity
public class Planet {
//...
@Transient
private Logger logger = LoggerFactory.getLogger( Planet.class );
//...
public void setNeighbour( Direction direction, Planet neighbour ) {
logger.info( "Attempt to set neighbour " + neighbour + " at " + direction );
}
}
Im Produktiv-Environment können Sie den Logging-Level dann je nach Bedarf hoch- oder runtersetzen.
Field-based Autowiring
Sie können eine Spring-gemanagedte Klasse (z.B. ein Repository oder einen Service) über @Autowired
automatisch
instanziieren lassen. Dafür gibt es mehrere grundsätzliche Wege. Beim Field-based Autowiring wird @Autowired
direkt über der Variablendeklaration angegeben:
public class PlanetApplicationService {
@Autowired
private PlanetRepository planetRepository;
//...
}
Damit kann man die Klasse PlanetApplicationService
aber nicht ohne das Spring-Framework nutzen, z.B. wenn man
mit einem Mock testen will. Besser ist Constructor-based Autowiring:
public class PlanetApplicationService {
private PlanetRepository planetRepository;
@Autowired
public PlanetApplicationService( PlanetRepository planetRepository ) {
this.planetRepository = planetRepository;
}
//...
}
Hier werden die Parameter des Konstruktors “autowired”. Man kann die Klasse dann auch ohne Spring instanziieren.
Häufig gemachte Fehler
Die nachfolgenden Patterns führen tatsächlich unter Umständen dazu, dass eine Spring-Applikation nicht mehr läuft,
also z.B. die allseits beliebte Failed to load Application Context
Exception zu werfen.
Entity ohne Repo
Zu jedem Entity, das ein Aggregate Root ist, muss es ein Repository (Interface, z.B. von CrudRepository
abgeleitet)
geben.
Innere Entities eines Aggregates können Sie ohne Repo machen, wenn Sie eine cascade
Anweisung in die Beziehung
aufnehmen. Also: Wenn R
das Root ist, und I
das innere Entity, dann gibt es z.B. @OneToMany
Deklaration für
eine List<I>
in R
. Bei dieser Deklaration muss dann ein CascadeType.MERGE
stehen - damit würde bei
Persistenzoperationen auf R
die eingeschalteten Objekte vom Typ I
mit behandelt.
Soweit die “reine Lehre”. Allerdings - jetzt kommt die Einschränkung - besonders praxistauglich finde ich das nicht.
Ich würde gern mal Produktiv-Code sehen, der innere Entities hat - wäre mal ein kleines feines Forschungsthema.
Ich würde fast wetten, dass man da doch doch Repos macht, und einfach per Konvention diese Repos nicht zur
“unabhängigen” Erzeugung innerer Entities nutzt. Einfach, weil man dadurch weniger Umwege / Einschränkungen / … im
Code hat.
Entity ohne ID
Wenn man bei einem Entity vergisst, eine ID mit @Id
zu spezifizieren, dann bekommt man in etwa folgende Exception:
Invocation of init method failed; nested exception is org.hibernate.AnnotationException:
No identifier specified for entity: thkoeln.st.st2praktikum.planet.domain.Planet
Versuch, ein Entity zu speichern, bei dem die ID schon bei einem anderen Entity vorhanden ist, oder null
Beides führt zu einer Constraint Violation in der unterliegenden relationalen Datenbank, und damit zu einer
Hibernate-Exception.
Abgeleitetes Entity, und die Oberklasse ist nicht auch als Entity getaggt
Vererbung sollte man sich grundsätzlich überlegen, weil man sich ein paar Nachteile einhandelt, was enge Kopplung
und z.B. Serialisierbarkeit in REST-APIs angeht. Die Alternative ist Komposition (googeln Sie mal “Composition
over Inheritance” für mehr Hintergrund dazu).
Wenn man Entities ableitet, dann muss man aufpassen, dass auch der Parent als @Entity
getaggt ist.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class MyAbstractParentEntity {
@Id
@GeneratedValue
private UUID id;
//...
}
@Entity
public class ConcreteEntity extends MyAbstractParentEntity {
// id wird geerbt ...
}
@Entity
public class AnotherConcreteEntity extends MyAbstractParentEntity {
//...
}
Repo mit falschem Entity- oder ID-Typ
Ein Repository sieht wie folgt aus:
public interface PlanetRepository extends CrudRepository<Planet, UUID> {
}
Hier wird gern mal jeder möglich Unfug :-) in die spitzen Klammern geschrieben: Mal die Repo-Klasse selbst, mal
ein falscher ID-Typ (z.B. wenn das Entity UUID
als ID-Typ verwendet, im Repo aber Long
steht). Nicht immer
sind die Fehlermeldungen sofort verständlich.
Falscher Transaktion-Kontext, zu merken z.B. durch LazyInitializationException
Manchmal bekommt man aus seinem Domain-Layer-Code folgende Exception. Grund ist, dass der Aufruf außerhalb des
Transaktionskontexts ist, und das dynamische Nachladen (“Lazy Loading”) von Entity-Referenzen schlägt fehl.
org.hibernate.LazyInitializationException: could not initialize proxy
[thkoeln.st.st2praktikum.planet.domain.Planet#aa225c5c-d72f-4d15-bd90-547d818ef338] - no Session
Der gern genutzte Fix hier ist, den Fetchtype auf “eager” umzustellen. Damit wird das Lazy Loading ausgeschaltet,
und alle Referenzen werden direkt der Query des Objekts mit aus der Datenbank geladen. Das kann ok sein, kann bei
verschachtelten Strukturen aber auch sehr “teuer” werden.
@Entity
public class Planet {
//...
@OneToOne ( fetch = FetchType.EAGER )
private Planet northNeighbour;
//...
Alternativ, und meistens die bessere Variante, ist es, die Transaktionskontexte zu analysieren. Hier kann man
mit einer @Transactional
Annotation an Methoden und Klassen arbeiten. Muss man im Einzelfall sorgfältig analysieren.
Autowiring außerhalb von Spring gemanagedten Klassen
Versucht man ein Autowiring in außerhalb von Spring gemanagedten Klassen, dann funktioniert das nicht - beim Zugriff
erhält man eine NullPointerException
. Dazu zählen alle Klassen, die nicht mit @Service
, @Component
oder @Bean
getaggt sind - also auch Entities!
“Viel hilft viel” - @Entity
, @Embeddable
und @Service
gemeinsam an einer Klasse
Das war auch in einem Codebeispiel dabei - aber “viel hilft viel” gilt hier leider nicht.
@Embeddable
@Entity
@Component
public class Planet {
//...
@OneToOne ( fetch = FetchType.EAGER )
private Planet northNeighbour;
//...
EntityNotFoundException
für eine Entity-Referenz beim Speichern
Bekommt man eine EntityNotFoundException
für eine Entity-Referenz beim Speichern, dann wird möglicherweise
versucht, eine Referenz zu einem noch transienten Entity abzuspeichern. Hibernate bekommt eine ID, geht davon aus,
dass es die in der DB gibt, und bekommt dann eine Constraint Violation.
@OneToOne ( cascade = CascadeType.MERGE )
Wenn es aber gewollt ist, dass die Referenz ggfs. noch nicht persistiert ist, dann kann
man die Referenz entsprechen mit einer Cascade-Anweisung annotieren. Mit der “Merge” Direktive legt Hibernate das
noch transiente referenzierte Objekt dann vorab in der DB an.
Zu beachtende Hinweise für unsere Test-Library
Unsere Test-Library für Java und UML-Diagramme hat den Anspruch, mit allen praxisnahen und validen
Umsetzungen auch umgehen zu können. Es gibt aber Ausnahmen, die zwar valide wären, die unsere Lib
aber nicht “kann”.
Verwendung von Merge Nodes
und Decision Nodes
in Zustandsdiagrammen
Wenn in einem Zustandsdiagramm gewisse Zustandsübergänge erwartet werden, dann muss man leider auf
Merge Nodes
und Decision Nodes
verzichten, weil die Verbindungen sonst nicht erkannt werden.

(Das liegt daran, dass die Lib Verbindungen immer zwischen zwei Knotenelementen erwartet, und die
Verbindung nicht über die Merge / Decision Node
hinaus weiterverfolgt. Das zu ändern würde einen
Major Rewrite bedeuten.)