JPA Inheritance

Vererbung ist ein Grundkonzept Objektorientierter Programmiersprachen. Auch Java ist mit diesem Ansatz groß geworden. Viele Patterns und Best Practices haben sich etabliert und wer effektiv mit Java Software entwickeln möchte, kommt nicht um die Schlüsselwörter „extends“ und „implements“ herum.

Im Kontext von Spring Boot geschieht der Einsatz von Vererbung etwas „zielgerichteter“ als in allgemeinen Java-Projekten. Zumindest wenn man im Fahrwasser der Best Practices der Community bleibt. Dennoch gibt es sinnvolle Anwendungsfälle für Vererbung auch in Spring Boot Anwendungen. Eine davon sehen wir uns heute genauer an. Ich bin immer wieder überrascht, wie wenig bekannt diese wirklich elegante Möglichkeit ist, die uns die Bibliothek JPA zur Verfügung stellt.

Es war einmal…

Zum Entstehungszeitpunkt dieses Posts tagen die Ministerpräsidenten mit Bundeskanzlerin Merkel und werden wohl eine Verlängerung des corona-bedingten Lockdowns beschließen. Unser heutiges Anwendungsbeispiel wirkt daher wie ein Relikt aus längst vergangenen Zeiten. Oder ein hoffnungsvoller Blick in die Zukunft, je nach Betrachtungsweise ;-)

Wir sehen uns heute eine klassische IT-Konferenz an. Unser stark vereinfachtes Datenmodell soll drei Personengruppen umfassen, Teilnehmer, Ordner und Sprecher. Da alle Personengruppen beispielsweise ein Namensschild benötigen, macht es Sinn, dass sie sich gewisse Attribute, wie zum Beispiel Vor- und Nachname teilen. In Java ist das in wenigen Zeilen umgesetzt:

public class Attendee {
   private final String firstname;
   private final String lastname;
   ...
}

public class Steward extends Attendee {
   private final LocalDate birthday;
   private final String email;
}

public class Speaker extends Steward {
   private final Session session;
}

Stellen wir die Diskussionen mal hinten an, ob jeder Speaker wirklich nur eine Session halten kann oder ob es Sinn macht, die Sprecher von den Ordnern erben zu lassen. Ich denke, die Grundidee wird auf jeden Fall klar..

Polymorphie

So weit sind die Strukturen rein aus Java Sicht relativ einfach und decken den Anwendungsfall ab, dass wir für alle Anwesenden ein Namensschild drucken wollen: Wenn wir die Möglichkeit haben, an eine Liste mit allen Attendees zu kommen, haben wir alle Daten beisammen. Vorausgesetzt, wir erhalten in dieser Liste alle Attendees und Objekte von Klassen, die Attendee erweitern, also auch alle Instanzen von Steward und Speaker.

Genau hier wird es spannend: Wenn wir die Daten in einer Relationalen Datenbank persistieren wollen, müssen wir diese dazu bringen, eine polymorphe Abfrage zu unterstützen. Polymorphie bedeutet hier, dass eben die erweiterten Klassen mit inkludiert werden.

Bevor wir uns ansehen, wie sich das in der Praxis umsetzen lässt, muss ich noch eine Lanze für das Konzept brechen: Natürlich könnte man für den geschilderten Anwendungsfall auch drei Queries schreiben, die Elemente hintereinander in eine Liste schreiben und hätte das gleiche Ergebnis. Spätestens bei Sortierung und Paginierung bekommen wir damit aber ernsthafte Probleme. Schauen wir uns daher einfach an, wie einfach wir mit JPA Bordmitteln die Polymorphie geschenkt bekommen.

Unterschiedliche Strategien

JPA bietet uns die Möglichkeit, die im Java-Code umgesetzte Vererbung auch innerhalb der Datenbank zu etablieren. Dafür gibt es drei Strategien, die wir uns der Reihe nach ansehen werden. Bei einer bewährten Bibliothek wie JPA können wir davon ausgehen, dass jede der Strategien ihre Berechtigung hat. Wie so oft in der Softwareentwicklung gilt es also, den für die gegebene Problemstellung optimalen Ansatz zu identifizieren.

Single Table

Die einfachste Strategie ist, einfach alle potenziell vorkommenden Felder zusammen in eine Tabelle zu schreiben. Für unser obiges Beispiel sieht das dann so aus:

diagram single table

Jetzt müssen wir JPA noch erklären, wie diese Datenbankstruktur mit unseren Java Klassen zusammenspielt. Wie üblich verwenden wir dazu die entsprechenden Annotationen.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

@DiscriminatorColumn(name = “type”)
public abstract class Person {
   @Id
   private long id;
   @Column
   private final String firstname;
   @Column
   private final String lastname;
}

@DiscriminatorValue(“attendee”)
public class Attendee extends Person {}

@DiscriminatorValue(“steward”)
public class Steward extends Person {
   @Column
   private LocalDate birthday;
   …
}

Grob gesagt: JPA nutzt die Tabellenspalte „type“ um die einzelnen Datensätze den Entities zuzuordnen. Dies teilen wir der übergeordneten Entity mit der Annotation @DiscriminatorColumn(name = “type”) mit. Die Kind-Entities erhalten mit der Annotation @DiscriminatorValue das Mapping auf die Werte, die in der Spalte „type“ vorkommen können.

Vorteile Single Table

• Sehr schlanke Struktur in der Datenbank

Nachteile Single Table

• Alle speziellen Spalten müssen nullable sein, das führt leicht zu inkonsistenten Daten

• Starke strukturelle Unterschiede zwischen Datenbankstruktur und Java-Code

Abgeschreckt durch die potenzielle Inkonsistenz der Daten? Bitte trotzdem weiterlesen! Es gibt noch andere Ansätze…

Table per Class

Bei der Strategie Table per Class wird jede Entity durch eine eigene Datenbanktabelle repräsentiert. Dadurch ergibt sich das folgende Schema:

diagram table per class

Auf der Java-Seite wird dieses Schema mit leicht modifizierten Entities aufgerufen:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Person {
   @Id
   private long id;

   @Column
   private String firstname;

   @Column
   private String lastname;
}

@Entity
public class Attendee extends Person {}

@Entity
public class Steward extends Person {
   @Column
   private LocalDate birthday;
   …
}

Die Strukturen der Entities ähneln denen für die Strategie Single Table sehr und durch den Wegfall der Typ-Spalte wird die ganze Struktur sogar noch ein kleines bisschen schlanker. Diesen Preis zahlen wir jedoch durch die redundanten Strukturen im Datenbankschema.

Vorteile Table per Class

• Schlanke, transparente JPA Struktur

• Datenintegrität sichergestellt

Nachteile Table per Class

• Gemeinsame Entity-Felder müssen im Datenbankschema synchron gehalten werden

• Generell ist die strukturelle Redundanz im Schema eigentlich zu vermeiden

Immer noch nicht so ganz von der JPA Inheritance überzeugt? Eine sehr schöne Strategie habe ich noch ;-) Vermutlich die sauberste aber auch die mit der größten Komplexität…

Joined

Die Gute Nachricht für alle Struktur-Enthusiasten: Es gibt mit der Inheritance Strategy Joined tatsächlich einen Ansatz, mit dem Entities und Datenbankschema fast synchron sind. Dafür brauchen wir allerdings einige Tabellen und Beziehungen:

diagram joined

An sich nicht kompliziert, ist einfach etwas mehr Struktur. Dafür können wir uns bei den Entities auf Gewohntes zurückziehen. Bevor ihr jetzt alle möglichen Diff-Tools herauskramen müsst: Abgesehen von der JPA Strategy sind die Entites komplett gleich wie für Table per Class.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Person {
   @Id
   private long id;

   @Column
   private String firstname;

   @Column
   private String lastname;
}

@Entity
public class Attendee extends Person {}

@Entity
public class Steward extends Person {
   @Column
   private LocalDate birthday;
   …
}

Natürlich ist auch hier nicht alles Gold, was glänzt:

Vorteile Joined

• Schlanke, transparente JPA Struktur

• Konsistenz sichergestellt

• Nur minimale Unterschiede zwischen Datenbankschema und JPA Entities

Nachteile Joined

• Bei jedem Aufruf einer speziellen Entity (z. B. Steward) wird implizit ein Join in der Datenbank ausgeführt, das hat einen Einfluss auf die Performance

• Hohe Komplexität des Schemas

Und nun?

Vorweg: Ich bin der Meinung, dass man sich in jedem Fall mit dem Thema JPA Inheritance auseinandergesetzt haben sollte, wenn man Spring Boot mit JPA im Einsatz hat.

Allen Strategien ist gemein, dass wir vier vollwertige Entities zur Verfügung haben, also auch mit der eigentlich abstrakten Klasse Person Abfragen fahren können. Mit einem einfachen Spring Data Repository können so zum Beispiel alle Personen abgefragt werden:

@Autowired
private PersonRepository personRepository;



List<Person> persons = personRepository.findAll();

Die Liste persons würde Elemente vom Typ Attendee, Steward und Speaker enthalten. Das eröffnet viele Möglichkeiten, Daten schon in der Datenbank auszufiltern und dadurch die Gesamtperformance zu verbessern.

Grenzen

Natürlich ist auch die Inheritance kein Allheilmittel. JPA und seine Eigenheiten gibt einige Grenzen vor. Während bei einer nativen Query im Fall Single Table die Sortierung nach einer speziellen Spalte (also einer nullable Spalte) möglich ist, verbietet uns JPA dies. Wir können also nicht ohne weiteres alle Personen abfragen und nach Geburtsdatum sortieren, sofern vorhanden. Zumindest nicht mit den JPA Bordmitteln…

Einfach mal ausprobieren!

Abgesehen von solchen Spezialfällen habe ich den Einsatz von JPA Inheritance in Projekten noch nie bereut, bin sogar der Meinung, dass der Code dadurch übersichtlicher und wartbarer wird. In diesem Sinne wünsche ich viel Spaß beim Herumexperimentieren!

Spring-Boot News

Bleibe auf dem aktuellsten Stand mit unseren kostenlosen Spring-Boot updates. So wirst Du direkt informiert, wenn wir einen neuen Artikel veröffentlichen.
Kein Spam, kein Bullshit, nur Spring-Boot Insider-Wissen, versprochen.

Noch keine Kommentare vorhanden.

Was denkst du?

© 2020 Mischok