Im ST2-Praktikum begegnen uns die nachfolgenden Anti-Patterns häufig. Will man eine DDD-Architektur mit Spring JPA umsetzen, dann sollte man diese Patterns vermeiden.
equals( Object obj )
ImplementationSystem.out.println
statt Verwendung des Loggersnull
LazyInitializationException
@Entity
, @Embeddable
und @Service
gemeinsam an einer KlasseEntityNotFoundException
für eine Entity-Referenz beim SpeichernWir gehen zunächst die nachfolgenden Anti-Patterns gemeinsam durch und diskutieren sie in der großen Runde. Dann gehen Sie in Kleingruppen in Ihren eigenen (Praktikums-)Code und schauen, ob Sie die Anti-Patterns auch bei sich finden. Refactoren Sie Ihren Code.
Die nachfolgenden Patterns führen nicht zu Fehlern im Code, aber zu schlechtem Code.
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.
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?
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.
//...
}
//...
}
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 :-(.
Thematischer Schwenk: Value Objects (… @Embeddable
-Klassen …) sind per Definition immutable. Sie haben also
keine public Setter! Sonst kann man sie von außen ändern.
equals( Object obj )
ImplementationZwei 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.
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.
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 LoggersSystem.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.
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.
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.
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.
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
null
Beides führt zu einer Constraint Violation in der unterliegenden relationalen Datenbank, und damit zu einer Hibernate-Exception.
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 {
//...
}
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.
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.
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!
@Entity
, @Embeddable
und @Service
gemeinsam an einer KlasseDas 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;
//...
Mehrdimensionale Collections, z.B. HashMaps, können in JPA nicht direkt persistiert werden. Wie sollte eine Tabellenstruktur dafür aussehen?
EntityNotFoundException
für eine Entity-Referenz beim SpeichernBekommt 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.