Vorgaben und Konventionen

Die folgenden Vorgaben und Konventionen sorgen für eine einfache und einheitliche Verwendung von JPA und Hibernate.

1. Verwendung von JPA und Hibernate

1.1. JPQL für Datenbank-Queries nutzen

Für Datenbank-Queries stellt JPA die Java Persistence Query Language (JPQL) bereit. In JPQL werden Queries über Objekte und Variablen, nicht über Tabellen und Felder, definiert. Wann immer möglich sollten JPQL-Queries statt SQL-Queries verwendet werden. Der einzige Grund für die Verwendung von SQL ist die Verwendung von datenbankspezifischen SQL-Features, welche durch JPQL nicht angeboten werden.

1.1.1. Nichtfunktionale Aspekte von Queries

Die folgenden Vorgaben für Queries verhindern negative Auswirkungen auf die Stabilität, Verfügbarkeit oder Sicherheit von Anwendungen:

  • Der %-Operator ist nach Möglichkeit zu vermeiden. Es können leicht lang laufende Queries entstehen, welche die Anwendung blockieren und die Datenbank unnötig belasten.

  • Für rein lesende Zugriffe und feste Auswertungen sind nach Möglichkeit Views zu verwenden und die Berechtigungen entsprechend zu setzen. Dadurch kann der Zugriff auf die tatsächlich benötigten Daten gesteuert und eingeschränkt werden.

  • Bei der Formulierung von Queries sind die Eigenheiten des Optimizers der eingesetzten Datenbank zu beachten.

  • Es ist darauf zu achten, dass Queries durch Indizes in der Datenbank unterstützt werden.

  • Bei der Definition von Queries ist darauf zu achten, dass nicht zu viele Daten selektiert werden. Im Zweifel, insbesondere bei Queries, die aus Benutzereingaben erzeugt werden, sollte die Anzahl der selektierten Datensätze beschränkt werden.

  • Um SQL-Injection Attacken zu verhindern, sollen Named-Queries oder Criteria-Queries verwendeten werden, bei denen Hibernate für ein Escaping der Query-Parameter sorgt.

1.2. Optimistisches Locking standardmäßig verwenden

Standardmäßig ist optimistisches Locking zu verwenden. Dieser Vorgehensweise liegt die Annahme zugrunde, dass konkurrierende schreibende Zugriffe in einem Backend nicht oder höchstens in Ausnahmefällen vorkommen. Im Backend ist keine explizite Fehlerbehandlung (etwa durch das Zusammenführen der Daten) zu implementieren. Die geworfene Ausnahme ist, geschachtelt in eine Ausnahme des Backends, an den Aufrufer weiterzugeben.

Hibernate User Guide: Optimistic Locking.

1.2.1. Pessimistisches Locking bei Bedarf verwenden

Falls für einen Teil der Entitäten konkurrierende schreibende Zugriffe wahrscheinlich sind, ist für genau diese Entitäten pessimistisches Locking zu verwenden.

Hibernate User Guide: Pessimistic Locking.

1.3. Einsatz eines Second-Level-Caches

Ein Second-Level-Cache speichert Daten zwischen Transaktionen oder Sitzungen und ermöglicht es, sie anstatt eines Datenbankzugriffs wiederzuverwenden. Dies kann die Leistung eines IT-Systems erheblich verbessern, indem es die Anzahl der Datenbankzugriffe reduziert. Hibernate bietet einen Second-Level-Cache auf Ebene der Session Factory an.

Einsatz eines Second-Level-Cache

Sinnvoll ist der Einsatz eines Second-Level-Caches nur, wenn die darin enthaltenen Daten sehr häufig gelesen, aber nur selten geändert werden.

Weitere Details zum Einsatz von Caches bietet die offizielle Hibernate-Dokumentation zu Caching.

1.3.1. Betrachtung der Vorteile, Herausforderungen und Risiken

Die folgende Betrachtung geht von einer Caching-Lösung als Bestandteil eines IT-Systems aus. Sie betrachtet keine verteilten Caches.

Der Einsatz eines Second-Level-Caches kann die Erfüllung der folgenden Qualitätskriterien fördern.

Tabelle 1. Second-Level-Cache: Vorteile nach Qualitätskriterien (ISO 25010)
Qualitätskriterium Vorteile

Leistungsfähigkeit (Performance Efficiency)

IT-Systeme können schneller antworten und dabei Ressourcen schonen, da der Cache Datenbankzugriffe einspart.
IT-Systeme können einer höheren Last standhalten, da der Cache Lastspitzen abfedern kann.

Zuverlässigkeit (Reliability)

IT-Systeme können Daten auch bei temporären Ausfällen der Datenbank zuverlässiger liefern.

Demgegenüber kann der Einsatz eines Second-Level-Caches zu Herausforderungen und Risiken bei folgenden Qualitätskriterien führen.

Tabelle 2. Second-Level-Cache: Herausforderungen und Risiken nach Qualitätskriterien (ISO 25010)
Qualitätskriterium Herausforderungen / Risiken

Leistungsfähigkeit (Performance Efficiency)

Die im Cache gehaltenen Daten führen zu einem höheren Speicherverbrauch.

Funktionale Eignung (Functional Suitability)

Der Cache kann veraltete und damit potenziell inkonsistente Daten liefern.

Wartbarkeit (Maintainability)

Komplexe Cache-Konfigurationen können schwer zu debuggen und zu testen sein.

1.3.2. Empfehlungen zum Einsatz

Die IsyFact gibt die folgenden, generellen Empfehlungen beim Einsatz von Second-Level-Caches, aufgeschlüsselt nach Qualitätskriterien.

Tabelle 3. Second-Level-Cache: Empfehlungen nach Qualitätskriterien (ISO 25010)
Qualitätskriterium Empfehlungen

Leistungsfähigkeit (Performance Efficiency)

Nur so viel wie nötig an Daten im Cache halten.
Gezielt Cache-Regionen und passende Cache-Strategien nutzen.

Funktionale Eignung (Functional Suitability)

Cache Eviction (z.B. bei Änderungen an den Daten) sorgfältig planen.

Wartbarkeit (Maintainability)

Cache-Konfiguration im Systementwurf dokumentieren.

1.4. Verwendung von Hibernate Filtern

Wenn in einer Anwendung viele wiederkehrende Abfragen auf Entitäten erfolgen, können Hibernate Filter eingesetzt werden, um die "WHERE-Klauseln" der Abfragen zu vereinfachen oder zu ersetzen.

Hibernate Filter vereinfachen wiederkehrende Abfragen. Sie können dynamisch gesetzt sowie pro Hibernate Session aktiviert und deaktiviert werden. Sie können an Entitäten und durch Collections realisierten Assoziationen definiert werden.

Verwendung von Hibernate Filtern

Wenn das fachliche Datenmodell variable Sichtbarkeitsregeln in größerem Umfang benötigt, sollten diese mit Hibernate Filtern umgesetzt werden.

Außer der Annotation @Filter gibt es auch die Annotation @Where, die jedoch immer aktiv ist und eine statische Filterung durchführt. Im folgenden Beispiel würde ihr Einsatz dazu führen, dass die Anwendung generell keine gelöschten Items abfragen könnte. Deshalb wird Annotation @Where nur im Ausnahmefall empfohlen und hier nicht näher betrachtet.

1.4.1. Beispiel für die Verwendung von Hibernate Filtern

Zur Veranschaulichung wird ein Beispiel für die Verwendung von Hibernate Filtern aufgeführt. Es gibt eine Entität User und eine Entität Item sowie eine 1-zu-n-Assoziation zwischen User und Item. Die Entität Item hat ein Attribut deleted, das als Soft Delete verwendet wird. (Dies ist keine Empfehlung, Soft Deletes zu verwenden.)

Listing 1. Hibernate Filter auf Ebene von Klassen und Collections
@Entity
//Definition Hibernate Filter
@FilterDef(
     name="aktuellesItem",
     parameters = @ParamDef(
         name="geloescht",
         type="boolean"
    )
)
//Beispiel für Hibernate Filter auf Klassen-Ebene
@Filter(
     name="aktuellesItem",
     condition="item_geloescht = :geloescht"
)
public class Item {
     @Id
     private Long id;

     @Column(name = "item_geloescht")
     private boolean deleted;
}

@Entity
public class User {
    @Id
    private Long id;

    @OneToMany
    @JoinColumn(name = "user_id")
    //Beispiel für Hibernate Filter auf Collection-Ebene
    @Filter(
        name="aktuellesItem",
        condition="item_geloescht = :geloescht"
    )
    private Set<Item> items;
    public Set<Item> getItems(){
	    return items;
    }
}
Listing 2. Zugriff auf Klasse und Collection mit Hibernate Filter
//Zugriff per Spring Data Repository
public class FilterExample {

    @Autowired
    private ItemRepository itemRepository;

    @Autowired
    private UserRepository userRepository;

    public void howToUseFilters() {

        // Hibernate Filter sind standardmäßig deaktiviert.
        List<Item> alleItems = itemRepository.findAll();
        // alleItems.size() == 3
        User user = userRepository.findById(1).orElse(null);
        // user.getItems().size() == 3

        // Hibernate Filter aktivieren
        entityManager
            .unwrap(Session.class)
            .enableFilter("aktuellesItem")
            .setParameter("geloescht", false);

        // Mit aktiviertem Filter wird eine Entität gefiltert.
        List<Item> aktuelleItems = itemRepository.findAll();
        // aktuelleItems.size() == 2
        // user.getItems().size() == 2
    }
}
Das Suchen per Identifier (z.B. mittels itemRepository.findById(1)) wendet keine Filter an, siehe filtering entities and associations.

1.5. Verbot von Bulk-Queries

JPA bietet über die Methode query.executeUpdate() die Möglichkeit in JPQL formulierte DELETE- und UPDATE-Statements, sog. Bulk-Queries, auszuführen. Die Nutzung solcher Bulk-Queries ist verboten. Wo aus Performancegründen massenhafte DELETE- oder UPDATE-Statements direkt in der Datenbank benötigt werden, können native SQL-Anweisungen verwendet werden. Sofern bei solchen Bulk-Operationen kaskadierende Änderungen benötigt werden (z.B. weil Kind-Tabellen mitgelöscht werden sollen), müssen entsprechende Constraints in der Datenbank angelegt werden.

Begründung: Hibernate erzeugt bei der Ausführung von BULK-Queries unter bestimmten Umständen zur Laufzeit implizit Hilfstabellen (temporäre Tabellen mit dem Präfix HT_).

Dies führt dazu, dass der Datenbank-User der Anwendung entsprechende CREATE TABLE-Rechte benötigt, was i.d.R. nicht zugelassen ist. Weiterhin führt die Nutzung der temporären Tabellen in vielen Fällen zu Performance-Problemen.

Um die Einhaltung dieser Anforderung sicherzustellen, sollten auch in der Entwicklung bzw. bei frühen Tests die Rechte auf die Testdatenbanken entsprechend beschränkt werden.

2. Definition des O/R-Mappings

2.1. Nutzung von Annotationen

Die Definition des Mappings wird über JPA-Annotationen in den Entitäten durchgeführt. Darüber hinaus bietet Hibernate eigene Annotationen für Features an, die Hibernate über JPA hinaus bereitstellt. XML-Konfiguration sollte nur in Ausnahmefällen noch nötig sein.

2.2. Identifizierende Attribute verwenden

Falls für eine Entität genau ein identifizierendes Attribut existiert, ist dieses sowohl in der Datenbank als auch im Hibernate Mapping als Primärschlüssel zu verwenden. Künstliche IDs sind nur dann als Schlüssel zu verwenden, wenn kein identifizierendes Attribut für die Entität vorliegt oder nur mehrere Attribute zusammen die Entität eindeutig identifizieren. Zusammengesetzte Schlüssel dürfen nicht verwendet werden.

Das identifizierende Attribut darf beliebige Typen besitzen. Es dürfen, neben numerischen Werten, auch Zeichenketten oder Datumsangaben sein.

2.2.1. Konfiguration künstlicher IDs

Künstliche IDs werden in JPA mit den Annotationen @Id und @GeneratedValue markiert. Der Parameter strategy der Annotation @GeneratedValue muss in jedem Fall AUTO sein.

Es muss unbedingt darauf geachtet werden, das Inkrement (INCREMENT BY) der entsprechenden Datenbanksequenz auf denselben Wert einzustellen, der auch im Parameter allocationSize der Annotation @SequenceGenerator angegeben ist.
Listing 3. Konfiguration der ID und Sequenz
@Entity
public class MyEntity {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO, generator="my_seq")
    @SequenceGenerator(name="my_seq",sequenceName="MY_SEQ", allocationSize=50)
    private int id;

}

2.3. Definition von Assoziationen

2.3.1. 1-zu-n und n-zu-n Assoziationen

Eine 1-zu-n-Assoziation (siehe Collection Mapping) ist in der Regel als unsortierte Menge (Set) zu definieren, da in dieser keine Reihenfolge definiert ist. Wird von der Anwendung eine Sortierung benötigt und sind alle für die Sortierung benötigten Attribute in der Entität enthalten, dann kann auch eine Liste (List) verwendet werden, da die Datenbank effizienter sortieren kann als eine Java-Implementierung.

Listing 4. Definition von 1-zu-n-Assoziationen
@Entity
public class MyEntity {

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "unsorted_id")
    private Set<UnsortedEntity> unsortedEntities = new HashSet<>();

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "sorted_id")
    @OrderBy("field ASC")
    private List<SortedEntity> sortedEntities = new ArrayList<>();

}

Bei einer 1-zu-n oder n-zu-n-Assoziation lädt Hibernate alle zugehörigen Entitäten, wenn es die Assoziation initialisiert. Das kann je nach Menge und Größe der assoziierten Entitäten eine gewisse Zeit dauern und die Antwortzeit von Anfragen an das Backend deutlich beeinflussen.

2.3.2. Bidirektionale Assoziationen

Bidirektionale Assoziation beschreibt die Beziehung zwischen zwei Entitäten, wobei jede Entität einen Verweis auf die andere Entität besitzt. Sie ermöglicht es ihnen, von einer Entität zu einer anderen Entität zu navigieren, die mit ihr verbunden ist, und umgekehrt.

Es gibt vier verschiedene Arten der bidirektionalen Assoziation, die wie folgt aussehen:

  • Bidirektionale 1-zu-1-Verknüpfung (one-to-one),

  • Bidirektionale 1-zu-n-Verknüpfung (one-to-many),

  • Bidirektionale n-zu-1-Verknüpfung (many-to-one),

  • Bidirektionale n-zu-n-Verknüpfung (many-to-many).

Wenn eine bidirektionale Assoziation gebildet wird, muss sichergestellt werden, dass beide Seiten zu jeder Zeit synchron sind.

Hibernate User Guide: Bidirectional @OneToMany

2.3.3. Lazy Loading standardmäßig verwenden

Standardmäßig soll für alle Assoziationen Lazy Loading verwendet werden. Bytecode-Manipulationen für Lazy Loading sollen nicht verwendet werden.

JPA empfiehlt Lazy Loading für alle 1-zu-n- und n-zu-m-Assoziationen und Eager Loading für n-zu-1- oder 1-zu-1-Assoziationen. Hibernate, im Gegensatz, empfiehlt Lazy Loading für alle Assoziationen.

Um Lazy Loading auch für 1-zu-1-Assoziationen einzuschalten, wird das Attribut fetch der Annotation @OneToOne auf FetchType.LAZY gesetzt. Damit das Lazy Loading über Proxies funktioniert, darf die Assoziation nicht optional sein.

Listing 5. Lazy Loading bei 1-zu-1-Assoziationen
@Entity
public class MyEntity {

    @OneToOne(optional = false, fetch = FetchType.LAZY)
    private OtherEntity otherEntity;

}

Für n-zu-1-Assoziationen wird genauso verfahren und das Attribut fetch auf FetchType.LAZY gesetzt.

Listing 6. Lazy Loading bei n-zu-1-Assoziationen
@Entity
public class MyEntity {

    @ManyToOne(fetch = FetchType.LAZY)
    private OtherEntity otherEntity;

}

Anders als bei 1-zu-1-Assoziationen ist hier erlaubt, Eager Loading zu verwenden, wenn dieses Verhalten Sinn ergibt und keine negativen Auswirkungen zu erwarten sind. Typische negative Auswirkungen sind N+1-Queries (die umgekehrte Assoziation von OtherEntity zu MyEntity benutzt Eager Loading) oder das Auslesen zu vieler Daten (OtherEntity besitzt viele Assoziationen mit Eager Loading).

2.4. Vererbungshierarchien

Vererbungshierarchien können in relationalen Datenbanken nicht direkt umgesetzt werden.

Für alle Strategien zur Abbildung gilt, dass die abzubildende Vererbungshierarchie nicht zu umfangreich sein sollte. Datenbankzugriffe auf Tabellen mit großen Hierarchien sind meistens wenig performant. Außerdem lässt sich die Vererbungshierarchie anhand der Datenbanktabellen entweder nicht oder nur schwer erkennen und die Tabellen können unübersichtlich werden.

Vererbungshierarchien im O/R-Mapping

Vererbungshierarchien zur Abbildung in relationalen Datenbanken sollten nur verwendet werden, wenn das fachliche Datenmodell dadurch optimal wiedergegeben wird. Sie sollten nur eine Oberklasse mit einigen Subklassen und höchstens zwei Vererbungsebenen umfassen.

Es werden zunächst die vier Strategien zur Abbildung von Vererbungshierarchien vorgestellt und Architekturregeln festgelegt.

2.4.1. Single Table per Class Hierarchy

Mit der Single Table per Class Hierarchy Strategie wird eine Vererbungshierarchie auf eine einzelne Datenbanktabelle gemappt. Die Tabelle hat eine Diskriminatorspalte. Anhand des Wertes dieser Spalte wird die spezielle Subklasse bestimmt, auf die eine bestimmte Zeile der Datenbank gemappt wird.

Verwendung der Single Table per Class Strategie

Die Single Table per Class Hierarchy Strategie sollte die Default-Strategie sein, weil sie performante Abfragen erlaubt.

Die Single Table per Class Hierarchy Strategie kann nicht angewandt werden, wenn für Spalten, die von Attributen der Subklassen gemappt wurden, Not-Nullable-Constraints zwingend erforderlich sind, s.a. Joined Subclass.

2.4.2. Joined Subclass

Eine weitere Strategie des O/R-Mappings von Vererbungshierarchien ist die Joined Subclass Strategie. Jede Klasse wird auf eine eigene Tabelle abgebildet.

Der Zugriff ist weniger performant als bei der Single Table per Class Hierarchy Strategie.

Verwendung der Joined Subclass Strategie

Wenn Not-Nullable-Constraints zwingend erforderlich sind und polymorphe Queries benötigt werden, ist die Joined Subclass Strategie eine gute Wahl. Ein weiteres Argument für diese Strategie sind Subklassen mit vielen Attributen.

2.4.3. Table per Concrete Class

Bei der O/R-Mappingstrategie Table per Concrete Class wird jede nicht abstrakte Klasse auf eine Datenbanktabelle abgebildet. Alle Attribute der Oberklasse werden als Spalten an alle Tabellen für die Subklassen angefügt.

Das Mapping zwischen Entitäten und Datenbanktabellen ist einfach, aber die Tabellen sind nicht normalisiert und der polymorphe Zugriff auf die Oberklasse ist kaum performant.

Verwendung der Table per Concrete Class Strategie

Die Table per Concrete Class Strategie sollte, wenn überhaupt, nur gewählt werden, wenn die anderen Strategien nicht passen und auf die Oberklasse keine oder nur wenig polymorphe Zugriffe zu erwarten sind.

2.4.4. Mapped Superclass

Es liegt bei der Mapped Superclass Strategie keine Vererbungshierarchie unter Entitäten vor, die Oberklasse ist keine Entität. Die Oberklasse dient nur der Strukturierung und Zusammenfassung von gemeinsamen Eigenschaften. Sie wird deshalb auch nicht auf eine Datenbanktabelle abgebildet. Ihre Attribute werden aber als Spalten an alle Tabellen der von ihr erbenden Entitäten angefügt.

Polymorphe Queries auf die Oberklasse sind nicht möglich.

Verwendung der Mapped Superclass Strategie

Diese Art der Vererbung von einer Java-Oberklasse auf Entitäten-Subklassen kann eingesetzt werden, wenn nur auf die Subklassen zugegriffen werden muss.

Es erspart die Wiederholung von Attributen in den Entitäten, aber nicht in den Datenbanktabellen.

2.4.5. Beispiele, Vor- und Nachteile

Die vier O/R-Mapping-Strategien werden in den folgenden Abschnitten genauer betrachtet mit ihren Vor- und Nachteilen.

2.4.5.1. Single Table per Class Hierarchy

Für die Single Table per Class Hierarchy Strategie wird ein Beispiel gezeigt. Bei den anderen Strategien wird auf Teile davon verwiesen.

Listing 7. Single Table per Class Hierarchy
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="personengruppe",
  discriminatorType = DiscriminatorType.INTEGER)
public class Person {
	//…
}

@Entity
@DiscriminatorValue("1")
public class Schueler extends Person {
    private Integer klassenstufe;
    public Integer getKlassenstufe() {
        return klassenstufe;
    }
	//…
}

@Entity
@DiscriminatorValue("2")
public class Lehrer extends Person {
    private BigDecimal gehalt;
    public BigDecimal getGehalt() {
        return gehalt;
    }
	//…
}
Listing 8. Polymorpher Zugriff
class PolymorphicAccessExample {

    @Autowired
    private PersonRepository personRepository;

    public void access() {
        List<Person> personen = personRepository.findAll();

        // Zugriff auf Attribute der Subklassen
        personen.forEach(person -> {
            if (person instanceof Schueler) {
                ((Schueler) person).getKlassenstufe();
            } else if (person instanceof Lehrer) {
                ((Lehrer) person).getGehalt();
            }
        });
    }

}

Vorteile

  • Auf die Datenbanktabelle kann polymorph zugegriffen werden.

  • Die Queries auf Ober- und Subklassen sind performant, da keine Joins erforderlich sind.

Nachteile

  • Auf Attribute von Subklassen kann kein Not-Nullable-Constraint gesetzt werden. Im Beispiel kann klassenstufe nicht auf not nullable gesetzt werden, denn wenn die gespeicherte Person ein Lehrer ist, ist klassenstufe null.

  • Falls Datenbankadministratoren z.B. bei Fehlern den Inhalt der Tabelle analysieren müssen, ist die Zugehörigkeit einzelner Spalten zu bestimmten Subklassen nicht allein aus der Datenbanktabelle ersichtlich. In diesem Fall ist es hilfreich, wenn für jede Klasse der Vererbungshierarchie ein View definiert wurde. Diese Views beeinflussen das O/R-Mapping nicht, denn sie werden dafür nicht verwendet.

2.4.5.2. Joined Subclass

Jede Klasse wird auf eine eigene Tabelle abgebildet, auch eine abstrakte Oberklasse, und enthält nur ihre eigenen Attribute als Spalten. Die Primärschlüssel-Ids der Subklassen sind gleichzeitig Fremdschlüssel für die entsprechenden Primärschlüssel-Ids der Oberklasse. Dadurch werden beim polymorphen Zugriff auf die Oberklasse die Sub-Entitäten per Join mit der Tabelle der Oberklasse gelesen (implizit per O/R-Mapper).

Die Oberklasse wird folgendermaßen annotiert:

Listing 9. Joined Subclass
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Oberklasse { }

Vorteile

  • Die Datenbanktabellen sind normalisiert.

  • Die Vererbungshierarchie ist ansatzweise erkennbar in den Datenbanktabellen.

Nachteile

  • Je nach Vererbungshierarchie sind performanzkritische Joins erforderlich beim Zugriff sowohl polymorph auf Ober- als auch auf Subklassen.

2.4.5.3. Table per Concrete Class

Die Oberklasse wird folgendermaßen annotiert:

Listing 10. Table per Concrete Class
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Oberklasse { }

Vorteile

  • Die Vererbungshierarchie ist an der Datenbank ansatzweise nachvollziehbar, zumindest dann, wenn die Oberklasse nicht abstrakt ist und auch gemappt wird.

  • Einfaches Mapping zwischen Entitäten und Datenbanktabellen.

Nachteile

  • Die Datenbanktabellen sind nicht normalisiert.

  • Beim polymorphen Zugriff auf die Oberklasse muss dies (implizit per O/R-Mapper) über eine UNION-Query geschehen oder eine eigene Query für jede Subklasse.

2.4.5.4. Mapped Superclass

Die Oberklasse wird folgendermaßen annotiert:

Listing 11. Mapped Superclass
@MappedSuperclass
public class Oberklasse { }

Vorteile

  • Einfaches Mapping zwischen Entitäten und Datenbanktabellen.

Nachteile

  • Ein polymorpher Zugriff ist nicht möglich.

  • Die Datenbanktabellen sind nicht normalisiert.

  • Die Vererbungshierarchie ist in der Datenbank nicht nachvollziehbar.

2.5. Behandlung von Datums- und Zeitangaben

Es werden die Datums- und Zeitklassen aus der Java 8 Date Time API verwendet. Hinweise zu deren Verwendung finden sich im Baustein Datum & Zeit. Der Baustein stellt zur Persistierung von Zeiträumen und ungewissen Datums- und Zeitangaben entsprechende Entitäten bereit.

Der folgende, hervorgehobene Absatz wird nur noch aus historischen Gründen erwähnt und ist obsolet.

Für alte Anwendungen, die nicht die Java 8 Date Time API, sondern noch java.util.Date verwenden, gelten die folgenden Vorgaben.

In der Datenbank erfolgt die Speicherung in einem Attribut vom Typ TemporalType.TIMESTAMP. Falls die Genauigkeit des Timestamp-Datentyps fachlich nicht gewünscht ist, kann der Typ TemporalType.DATE verwendet wird.

Hibernate erzeugt beim Laden der Daten aus der Datenbank implizit Objekte der Typen java.sql.Timestamp bzw. java.sql.Date für diese Attribute. Beide Typen sind von java.util.Date abgeleitet.

Vergleiche von Zeitangaben unterschiedlicher Genauigkeit sind jedoch problematisch:

  • Grundsätzlich darf der Vergleich nicht mittels equals durchgeführt werden, sondern immer mittels compareTo.

  • Ein Vergleich mit compareTo muss immer auf dem Attribut mit höherer Genauigkeit (also auf dem java.sql.Timestamp) aufgerufen werden.

Für Berechnungen, z.B. das Hinzuaddieren von Tagen, oder das Setzen von Feldern, ist der Daten-Typ java.util.Calendar zu verwenden. In diesem Fall wird im Anwendungskern temporär ein Objekt dieses Typs für das entsprechende Datum erzeugt.

2.6. Boolesche Variablen

Für die Ablage von booleschen Werten in der Datenbank ist stets ein numerisches Feld zu verwenden, kein Textfeld. Der Wert wird von Hibernate standardmäßig auf 1 für wahr und 0 für falsch abgebildet.

2.7. Enum-Variablen

Für die Ablage von Enum-Feldern persistenter Entitäten in der Datenbank sind in JPA zwei Modi vorgesehen, die jedoch beide mit Nachteilen verbunden sind:

ORDINAL

Die Enum-Ausprägungen werden durchnummeriert. Beim Hinzufügen oder Entfernen einer Enum-Ausprägung, die nicht die letzte ist, verschiebt sich die Bedeutung der Nummern und macht dadurch eine Datenmigration erforderlich.

STRING

Es wird der Java-Name der Enum-Ausprägung in der Datenbank abgelegt. Dies erzeugt eine enge Kopplung des Java-Quellcodes an die Datenbankinhalte. Während im Java-Quellcode lange, sprechende Namen bevorzugt werden, werden für die Ablage in der Datenbank kurze, Speicherplatz sparende Darstellungen präferiert.

Aufgrund der genannten Schwächen stellt der Baustein Util Annotationen und Hibernate UserTypes zur Verfügung, um Enum-Werte auf eine Zeichenkette in der Datenbank abzubilden.

2.8. Datenbankschema anfangs über hbm2ddl erzeugen

Für die Erstellung des Datenbankschemas wird empfohlen, es initial über Hibernate zu erzeugen. Die Konfiguration hierzu geschieht in der Datei application.properties der Anwendung.

Listing 12. Konfiguration zur automatischen Erzeugung von Datenbankschemas
spring.jpa.hibernate.ddl-auto=create

Grundsätzlich ist es möglich, sämtliche Tabellen-Eigenschaften (etwa auch die Feldlängen und Indizes) über Annotationen zu definieren und das Datenbankschema komplett durch hbm2ddl zu erzeugen. Ob das Datenbankschema während der Entwicklung stets generiert wird oder es nach einer initialen Generierung verändert und parallel gepflegt wird, ist je nach Komplexität des Schemas zu entscheiden.

Befindet sich die Anwendung in Produktion, dann muss die automatische Erzeugung von Datenbankschemas abgeschaltet sein.

Listing 13. Konfiguration zur Abschaltung der automatischen Erzeugung
spring.jpa.hibernate.ddl-auto=none

Eine Validierung des Datenbankschemas durch Setzen des Parameters auf validate findet nicht statt. Stattdessen wird Liquibase verwendet.

2.9. Vergabe von Indizes

Indizes sind ein wichtiges Element, um eine gute Performance des Datenbankzugriffs sicherzustellen. Indizes müssen dabei gezielt vergeben werden. Fehlende Indizes führen häufig zu einer schlechten Performance der Anwendung und belasten die Datenbank durch das vermehrte Auftreten von Full-Table-Scans sehr stark. Zu viele Indizes verschlechtern die Performance beim Schreiben von Datensätzen und verbrauchen unnötigen Speicherplatz.

Die tatsächlich notwendigen Indizes können letztendlich häufig nur in Produktion festgestellt werden. In dem Sinne ist es sinnvoll während der Entwicklung zunächst nur die sicher notwendigen Indizes anzulegen und diese später durch Erkenntnisse aus Lasttests und Produktion zu ergänzen.

Initial sind folgende Indizes vorzusehen:

  • ein Index auf jeder Spalte, die als Fremdschlüssel verwendet wird,

  • ein Index auf (fachliche) Schlüsselattribute, die sehr häufig im Rahmen der Verarbeitung genutzt werden (Beispiele: Nummer eines Registereintrags, Kennung einer Nachricht).