JPA Lifecycle Events

Lifcycle Event Aufrufe können mitunter etwas verwirrend sein, wie ich jüngst feststellen durfte. In folgendem Beispiel nehmen wir eine einfache OneToMany Relation:

Klassendiagramm

Hierbei sind die onSave Methoden jeweils als Livecyclelistener annotiert:

@PrePersist
@PreUpdate
private void onSave() {
    System.out.println("about to save MainEntity " + id);
}

Weiterhin ist die Relation zu SubEntities mit CascadeAll in MainEntity annotiert, damit die Abhängigen Entities automatisch gespeichert werden.

Zum Testen erzeugen wir als erstes einmal eine MainEntity mit einer SubEntity:

MainEntity me = new MainEntity(Long.valueOf(1)); // Id
Collection col = new ArrayList();
col.add(new SubEntity(me));
me.setSubEntities(col);
System.out.println("persisting...");
persist(me); // do persist stuff

Bei dem Aufruf wird zuerst der PrePersist Callback von MainEntity und danach von SubEntity. Die Ausgabe des Programms ist:

persisting...
about to save MainEntity 1
about to save SubEntity 1

Das ist auch genau das, was ich erwartet hätte. So könnte man vor dem Speichern die Abhängigen Collections aufräumen (z.B. die Rückrelation zu MainEntity eintragen).

Als nächstes erweitern wir das Programm um einen merge Aufruf. Vor dem Merge wird eine neue Entity zu der Collection zugefügt.

col.add(new SubEntity(me));
System.out.println("merging...");
merge(me);

Die Ausgabe des Programms für diesen Teil ist:

merging...
about to save SubEntity null
about to save MainEntity 1

Ups, das war nicht das, was ich erwarten würde. Wenn zu einer Existierenden Entity ein weiteres Element zu der OneToMany Collection zugefügt wird, so wird der Entsprechende Callback vor dem UpdateCallback der MainEntity aufgerufen.
Damit hat sich die Idee vom automatischen aufräumen von Collections erledigt.
Das Ganze funktioniert genau so, wenn eine attached MainEntity um eine SubEntity erweitert wird. Werden hingegen nur SubEntities, die bereits existieren aktualisiert, wird der Callback für diese erst nach der MainEntity aufgerufen.
Getestet mit TopLink und EclipseLink mit identischen Ergebnissen.

Agile Entwicklung

Ich habe neulich ein relativ übersichtliches Swing/JPA Projekt gebaut und hatte dabei den Luxus zeitlich recht frei zu sein.

Bei GUI-lastigen Programmen für Kunden ist es besonders wichtig, schnell einen Prototypen zu haben, den der Kunde ausprobieren kann. Die meisten Kunden kommen nicht aus der IT-Branche und sind es oft nicht gewohnt, alltägliche Abläufe zu abstrahieren. Es ist also ziemlich belanglos, wie gut die Vorgespräche waren, es muss schnell etwas Konkretes zum ausprobieren her, damit erfahren werden kann, was vorher besprochen wurde. Damit hat man dann eine großartige Grundlage für die ganz sicher nötigen Anpassungen, bis Kunden- und Enwicklervorstellung sich decken.

Provisorien halten ewig

Der Code von dem funktionsfähigen Prototypen sah schrecklich aus und auch nur wenigen Tage Pause hätten vermutlich dafür gesorgt, dass nichtmal ich weiß, was ich da geschrieben habe. Alle GUI Konstrukte für den Hauptdialog in einer Klasse, keine Zeile Dokumentation (abgesehen von den „FixMe“ und „ToDo“ Kommentaren) und redundanter Code.

Zwar hat das Programm die funktionalen Anforderungen erfüllt, war aber nicht mehr wirklich wartbar. Das ist allerdings für einen nicht Techniker nicht sichtbar. Der Kunde sieht die GUI, sieht, dass was passiert, wenn er irgendwo draufdrückt und ist ersteinmal zufrieden. Es wird jetzt sehr schwierig zu erklären, dass das Programm zwar funktioniert aber noch nicht „fertig“ ist. Für den Kunden ist das Projekt jetzt beendet.

Dass das auch auf InHouse Projekte von Firmen mit eigener IT-Abteilung zutrifft, sehe ich täglich bei meinen Beratungskunden. Da Erklären mir regelmäßig Administratoren den Unterschied zwischen „bösen“ und „guten“ Exceptions: Gute Exceptions werden kontinuierlich ins Log geschrieben und werden zu bösen Exceptions, wenn die Applikation den Server lahmlegt. Dass es sich meist um exakt die gleichen Exceptions handelt die nur zu unterschiedlichen Zeitpunkten (einmal kurz nach dem Start des Servers – einmal kurz vor dem Absturz) betrachtet werden, ist dabei uninteressant.

Refactoring gehört zum Projekt

Nun, mein GUI Projekt sieht inzwischen gut aus. Ich habe den redundanten Code zusammengefasst, das Eventsystem überarbeitet und den Hauptdialog in mehrere Klassen aufgeteilt.

Gerade bei den etwas altbackenen, geschwätzigen Sprachen wie Java ist dabei eine gute IDE ausgesprochen hilfreich. Was dafür jedoch noch wichtiger ist, sind gute Tests, damit man sicherstellen kann, dass man beim Aufräumen nicht die funktionalen Anforderungen zerschießt.

Ich kann jetzt auch nach längerer Pause auf den Code gucken und weiß, wozu er gut ist – ja ich weiß sogar, wo ich eventuelle Erweiterungen einbauen kann. Und die Erweiterungen kommen. Ein gutes Programm ist ab einem bestimmten Komplexitätsgrad niemals fertig. Es gibt immer eine Zusatzfunktion, die das Leben für den Benutzer angenehmer macht oder eben Aufgrund von Veränderungen im Benutzungsumfeld nötig wird. Bedürfnisse erzeugen Bedürfnisse.

Die erste Entwicklung hat jetzt mit dem Refactoring ca. 20% länger gedauert als die Erstellung des first Shot und wird bei der Erweiterung die Zeit sicherlich auf wenigstens die Hälfte reduzieren. Der zusätzliche Aufwand wird sich so mittelfristig rechnen. Auch nur mittelfristiges Denken ist aber leider in der Branche nicht mehr sehr üblich. Der Prototyp wird zum finalen Produkt, jede Erweiterung wird ein Abreißen und Neubauen mit gleichzeitigem schimpfen auf die Unfähigkeit des ursprünglichen Entwicklers, der das Projekt mit Erstellung des funktionalen Prototypen aus den Händen gerissen bekommt. Teuer und nicht sehr lehrreich ist dieses Vorgehen.

Für eine kontinuierliche Produktentwicklung gehört deswegen Refactoring zwingend zur Projektzeit und sollte von vorne herein mit eingeplant werden.

Die Kristallkugel

Agile Entwicklung basiert auf Kommunikation und ständiger Veränderung. Die gleichen Grundprinzipien, die Gesellschaft am Leben erhält. Könnte man mit dem klassischen Design First Ansatz ein System bauen, dass allen Marktanforderungen entspricht?

Dazu müsste man in die Zukunft blicken können. Müsste Anforderungen kennen, bevor sich diese im Markt etabliert haben. Man müsste die Folgebedürfnisse bereits vorwegnehmen können. Das führt üblicherweise nur zu einer Menge an Funktionen, die später keinen Interessieren und ein Fehlen von Funktionen, die mittelfristig wichtig sind. Egal wie gut man sein Produkt plant, es bleibt bei dem Grundsatz, dass Bedürfnisse Bedürfnisse erzeugen. Das ist der Motor der Welt. Somit ist es ziemlich beliebig, was ich für eine Funktionalität plane, es wird immer eine geben, die darauf aufbaut. Im Zweifelsfall finanziert man also mit dem Blick in die Kristallkugel einen Holzweg aus Mahagoni.

Es ist meist eine gute Idee, sich bei neuen Produkten ersteinmal auf das Problem zu beschränken, dass sie vornehmlich lösen sollen. Neue Anforderungen kommen im Betrieb von ganz allein.