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:

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:

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:

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!

Danke für's lesen.
Julius Mischok ist Geschäftsführer der Mischok GmbH in Augsburg. Seine Kernaufgaben sind Prozessentwicklung, sowie Coaching und Schulung der Entwicklungsteams. Aktuell fokussiert sich seine Arbeit auf die Frage, wie Software schnell und mit einer maximalen Wertschöpfung produziert werden kann. Julius hat Mathematik studiert und entwickelt seit fast zwei Jahrzehnten Java. Seine Erfahrung brachte er unter anderem in Softwareprojekten für BMW, Audi, Hilti, Porsche, Allianz, Bosch, und viele mehr ein.