Letzte Woche habe ich in der laufenden Blogserie über JPA-Implementierungsmuster die relativen Vorzüge des Feldzugriffs gegenüber dem Eigenschaftszugriff erörtert. Diese Woche werde ich mich mit den Wahlmöglichkeiten bei der Abbildung von Vererbungshierarchien in JPA beschäftigen. JPA bietet drei Möglichkeiten, Java-Vererbungshierarchien auf Datenbanktabellen abzubilden:
- InheritanceType.SINGLE_TABLE - Die gesamte Vererbungshierarchie wird in einer Tabelle abgebildet. Ein Objekt wird in genau einer Zeile in dieser Tabelle gespeichert und der in der
Diskriminatorspalte gespeicherteDiskriminatorwert gibt den Typ des Objekts an. Alle Felder, die nicht in einer Oberklasse oder einem anderen Zweig der Hierarchie verwendet werden, werden auf . Dies ist die von JPA verwendete Standardstrategie für die Vererbungszuordnung.NULL - InheritanceType.TABLE_PER_CLASS - Jede konkrete Entitätsklasse in der Hierarchie wird auf eine eigene Tabelle abgebildet. Ein Objekt wird in genau einer Zeile in der spezifischen Tabelle für seinen Typ gespeichert. Diese spezifische Tabelle enthält Spalten für alle Felder der konkreten Klasse, einschließlich aller geerbten Felder. Das bedeutet, dass Geschwister in einer Vererbungshierarchie jeweils ihre eigene Kopie der Felder haben, die sie von ihrer Oberklasse erben. Bei der Abfrage der Oberklasse wird eine UNION der einzelnen Tabellen durchgeführt.
- InheritanceType.JOINED - Jede Klasse in der Hierarchie wird als eigene Tabelle dargestellt, so dass keine Feldduplizierung auftritt. Ein Objekt wird über mehrere Tabellen verteilt gespeichert; eine Zeile in jeder der Tabellen, aus denen seine Klassenvererbungshierarchie besteht. Die is-a-Beziehung zwischen einer Unterklasse und ihrer Oberklasse wird als Fremdschlüsselbeziehung von der "Untertabelle" zur "Obertabelle" dargestellt und die zugeordneten Tabellen werden JOINed, um alle Felder einer Entität zu laden.
Einen schönen Vergleich der JPA-Vererbungsoptionen mit Bildern und einer Beschreibung der Option @MappedSuperclass finden Sie in der DataNucleus-Dokumentation. Die interessante Frage ist nun: Welche Methode funktioniert unter welchen Umständen am besten?
SINGLE_TABLE - Einzelne Tabelle pro Klassenhierarchie
Die Strategie SINGLE_TABLE hat den Vorteil, dass sie einfach ist. Zum Laden von Entitäten muss nur eine Tabelle abgefragt werden, wobei die Diskriminatorspalte zur Bestimmung des Typs der Entität verwendet wird. Diese Einfachheit hilft auch bei der manuellen Überprüfung oder Änderung der in der Datenbank gespeicherten Entitäten.
TABLE_PER_CLASS - Tabelle pro konkrete Klasse
Die TABLE_PER_CLASS-Strategie erfordert nicht, dass Spalten nullbar gemacht werden, und führt zu einem Datenbankschema, das relativ einfach zu verstehen ist. Infolgedessen ist es auch leicht zu überprüfen oder manuell zu ändern.
JOINED - Tabelle pro Klasse
Mit der JOINED-Strategie erhalten Sie ein schön normalisiertes Datenbankschema ohne doppelte Spalten oder unerwünschte nullbare Spalten. Als solche ist sie am besten für große Vererbungshierarchien geeignet, seien sie tief oder breit.
Sind das alle Optionen?
Zusammenfassend können Sie also sagen, dass die folgenden Regeln gelten, wenn Sie aus den Standardoptionen für die Vererbungszuordnung von JPA wählen:
- Kleine Vererbungshierarchie -> SINGLE_TABLE.
- Breite Vererbungshierarchie -> TABLE_PER_CLASS.
- Tiefe Vererbungshierarchie -> JOINED.
Was aber, wenn Ihre Vererbungshierarchie sehr breit oder sehr tief ist? Und was ist, wenn die Klassen in Ihrem System häufig geändert werden? Wie wir bei der Entwicklung eines persistenten Befehls-Frameworks und einer flexiblen CMDB für unser Java EE-Produkt zur Automatisierung der Bereitstellung, Deployit, festgestellt haben, können sich die konkreten Klassen am unteren Ende einer großen Vererbungshierarchie häufig ändern. Daher werden diese beiden Fragen oft gleichzeitig positiv beantwortet. Zum Glück gibt es eine Lösung für beide Probleme!
Blobs verwenden
Zunächst einmal ist festzustellen, dass die Vererbung eine sehr große Komponente des objektrelationalen Impedanzfehlers ist. Und dann sollten wir uns die Frage stellen: Warum bilden wir all diese oft wechselnden konkreten Klassen überhaupt auf Datenbanktabellen ab? Wenn sich Objektdatenbanken wirklich durchgesetzt hätten, wären wir vielleicht besser dran, wenn wir diese Klassen in einer solchen Datenbank speichern würden. So wie es aussieht, haben die relationalen Datenbanken die Erde geerbt, so dass das nicht in Frage kommt. Es könnte auch sein, dass für einen Teil Ihres Objektmodells das relationale Modell tatsächlich sinnvoll ist, weil Sie Abfragen durchführen und die Datenbank die (Fremdschlüssel-)Beziehungen verwalten lassen wollen. Aber für einige Teile sind Sie eigentlich nur an der einfachen Persistenz von Objekten interessiert. Ein schönes Beispiel ist das "persistierte Befehlsframework", das ich oben erwähnt habe. Das Framework muss allgemeine Informationen über jeden Befehl speichern, z.B. einen Verweis auf den "Änderungsplan" (eine Art Ausführungskontext), zu dem er gehört, Start- und Endzeiten, Protokollausgaben usw. Es muss aber auch ein Befehlsobjekt speichern, das die tatsächlich auszuführende Arbeit repräsentiert (in unserem Fall ein Aufruf von wsadmin oder wlst oder etwas Ähnlichem). Für den ersten Teil ist das hierarchische Modell am besten geeignet. Für den zweiten Teil reicht eine einfache Serialisierung aus. Wir definieren also zunächst eine einfache Schnittstelle, die von den verschiedenen Befehlsobjekten in unserem System implementiert wird:
public interface Befehl {
void execute();
}
Und dann erstellen wir die Entität, die sowohl die Metadaten (die Daten, die wir in einem relationalen Modell speichern möchten) als auch das serialisierte Befehlsobjekt speichert:
@Entität
public class CommandMetaData {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@ManyToOne
private ChangePlan changePlan;
private Date startOfExecution;
private Date endOfExecution;
@Lob
private String log;
@Lob
@Spalte(name = "COMMAND", aktualisierbar = false)
private byte[] serializedCommand;
@Transient
Private Command Befehl;
public CommandMetaData(Befehlsdetails) {
serializedCommand = serializeCommand(details);
}
public Befehl getCommand() {
if (Befehl != null) {
command = deserializeCommand(serializedCommand);
}
Befehl zurückgeben;
}
[... Rest ausgelassen ...]
}
Das Feld serializedCommand ist ein Byte-Array, das aufgrund der @Lob-Anmerkung als Blob in der Datenbank gespeichert wird. Der Spaltenname wird explizit auf "COMMAND" gesetzt, um zu verhindern, dass der Standard-Spaltenname "SERIALIZEDCOMMAND" im Datenbankschema auftaucht. Das Feld command ist als @Transient markiert, um zu verhindern, dass es in der Datenbank gespeichert wird. Bei der Erstellung eines CommandMetaData-Objekts wird ein Command-Objekt übergeben. Der Konstruktor serialisiert das Command-Objekt und speichert die Ergebnisse in dem Feld serializedCommand. Danach kann der Befehl nicht mehr geändert werden (es gibt keine setCommand() -Methode), so dass der serializedCommand als nicht aktualisierbar markiert werden kann. Dadurch wird verhindert, dass das ziemlich große Blob-Feld jedes Mal in die Datenbank geschrieben wird, wenn ein anderes Feld der CommandMetaData (z.B. das Log-Feld ) aktualisiert wird. Jedes Mal, wenn die getCommand-Methode aufgerufen wird, wird der Befehl, falls erforderlich, deserialisiert und dann zurückgegeben. Die getCommand-Methode könnte als synchronisiert markiert werden, wenn dieses Objekt in mehreren gleichzeitigen Threads verwendet wird. Einige Dinge, die Sie bei diesem Ansatz beachten sollten, sind:
- Die verwendete Serialisierungsmethode beeinflusst die Flexibilität dieses Ansatzes. Die Standard-Java-Serialisierung ist einfach, kann aber nicht gut mit sich ändernden Klassen umgehen. XML kann eine Alternative sein, aber das bringt seine eigenen Versionierungsprobleme mit sich. Die Wahl des richtigen Serialisierungsmechanismus bleibt eine Übung für den Leser.
- Obwohl es Blobs schon seit einiger Zeit gibt, haben einige Datenbanken immer noch Probleme mit ihnen. Die Verwendung von Blobs mit Hibernate und Oracle kann zum Beispiel schwierig sein.
- Bei dem oben vorgestellten Ansatz werden alle Änderungen, die nach der Serialisierung am Befehlsobjekt vorgenommen werden, nicht gespeichert. Die clevere Verwendung der @PrePersist und @PreUpdate Lebenszyklus-Hooks könnte dieses Problem lösen.
Dieser halbobjektbasierte/halbrelationale Datenbankansatz für die Persistenz hat sich für uns als sehr gut erwiesen. Mich würde interessieren, ob andere Leute den gleichen Ansatz ausprobiert haben und wie es ihnen ergangen ist. Oder ist Ihnen eine andere Lösung für diese Probleme eingefallen? Eine Liste aller Blogs zu JPA-Implementierungsmustern finden Sie in der Zusammenfassung der JPA-Implementierungsmuster.
Verfasst von
Vincent Partington
Contact



