Vorgaben und Konventionen

IFS-Logo Diese Seite ist ein Teil der IsyFact-Standards. Alle Inhalte der Seite, insbesondere Texte und Grafiken, sind urheberrechtlich geschützt. Alle urheberrechtlichen Nutzungs- und Verwertungsrechte liegen beim Bundesverwaltungsamt.

Creative Commons Namensnennung Die Nutzung ist unter den Lizenzbedingungen der Creative Commons Namensnennung 4.0 International gestattet.

Eines der Ziele des Bausteins JPA/Hibernate ist die einfach und einheitliche Verwendung von JPA und Hibernate. Dazu stellt der Baustein eine Menge an Vorgaben und Konventionen bereit, auf denen die Umsetzungen beruhen. Die Vorgaben und Konventionen ergänzen die allgemeinen Vorgaben zur Erstellung eines Persistenz-Klassenmodells in der Referenzarchitektur.

1. Aufbau der Fachkomponenten

Der Baustein JPA/Hibernate ergänzt den Aufbau der Fachkomponenten aus der Referenzarchitektur um die Verwendung von Spring Data JPA.

Data Access Objects (DAOs) werden über Spring Data Repositories abgebildet. Der Name eines Repositories leitet sich von derjenigen Entität ab, die über das Repository bearbeitet wird.

aufbau fachkomponente datenzugriff spring data.dn
Abbildung 1. Aufbau einer Fachkomponente des Datenzugriffs mit Spring Data JPA
Tabelle 1. Namenskonvention DAO: Spring Data Repository
Data Access Object: Spring Data Repository

Schema

<Entität>Repository

Beispiel

MyEntityRepository

2. Vorgaben zur Verwendung von JPA

Die folgenden Vorgaben sind bei der Verwendung von JPA in Anwendungen zu beachten.

2.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.

2.2. 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.

2.3. Lazy Loading standardmäßig verwenden

Standardmäßig verwendet Hibernate:

  • Lazy Loading (über dynamische Proxies) für alle 1-zu-n- und n-zu-m-Assoziationen und

  • Eager Loading für n-zu-1- oder 1-zu-1-Assoziationen.

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

2.4. Optimistisches Locking standardmäßig verwenden

Standardmäßig ist für Hibernate ein optimistisches Locking zu verwenden. Objekte werden bei dieser Locking-Strategie nicht per "select for update" gesperrt. Stattdessen wird am Ende der Transaktion geprüft, ob lokal veränderte Objekte parallel in der Datenbank geändert wurden. Ist dies der Fall, wird eine Ausnahme geworfen.

Dieser Vorgehensweise liegt die Annahme zugrunde, dass konkurrierende schreibende Zugriffe in einer Geschäftsanwendung nicht oder höchstens in Ausnahmefällen vorkommen. Sollte dies nicht zutreffen, muss explizites Locking verwendet werden. In der Anwendung ist keine explizite Fehlerbehandlung (etwa durch das Zusammenführen der Daten) zu implementieren. Die geworfene Ausnahme ist, geschachtelt in eine Ausnahme der Anwendung, an den Aufrufer weiterzugeben.

Um zu erkennen, ob sich das Objekt in der Datenbank verändert hat, empfiehlt Hibernate die Verwendung eines numerischen Versions-Felds in jeder Datenbank-Tabelle.

2.5. Explizites Locking bei Bedarf verwenden

Falls für einen Teil der Entitäten konkurrierende Zugriffe möglich sind, ist für genau diese Entitäten ein explizites (pessimistisches) Locking zu verwenden.

2.6. Aufrufübergreifendes Caching vermeiden

Caching-Strategien sind kein Teil der JPA-Spezifikation. Für das Definieren eines Cache muss deswegen auf Hibernate-spezifische Mechanismen zugegriffen werden.

Jeder Aufruf der Persistenzschicht geschieht innerhalb einer Transaktion. In der Regel läuft jeder Aufruf in einer eigenen Transaktion ab, weswegen kein Zustand und keine Daten zwischen zwei Aufrufen gehalten oder geteilt werden können.

Ist ein aufrufübergreifendes Caching dennoch notwendig, ist dies nicht in der Persistenzschicht und nicht mittels Hibernate durchzuführen. Hibernate bietet für das Caching von Objekten prinzipiell zwei Möglichkeiten.

Cache in der Hibernate-Session

Die Hibernate-Session ist an einen Thread gebunden. Die Nutzungsschicht verwendet für jede Anfrage einen neuen Thread (und damit eine frische Hibernate-Session). Deshalb kann dieser Cache höchstens im Rahmen einer Anfrage an das IT-System gelten. Diese Nutzung eines Cache ist nicht sinnvoll.

VM-weiter "2nd Level Cache"

Dieser Cache ist vor allem für unveränderliche, häufig verwendete Informationen (z.B. Schlüsseldaten) gedacht. In der IsyFact werden solche Daten jedoch bereits durch andere Mechanismen vorgehalten. Deshalb ist eine Verwendung dieses Cache ebenfalls unnötig.

Die Verwendung von über einen Aufruf hinausgehenden Cache ist deshalb zu vermeiden. Falls aufgrund spezieller Anforderungen trotzdem ein 2nd Level Cache benötigt wird, ist auf folgende Punkte zu achten:

  • Für den Cache ist eine gesonderte Cache-Region zu verwenden.

  • Nur unveränderliche Daten dürfen in den Cache.

  • Man kann nicht davon ausgehen, dass der Cache bei Änderungen der Objekte aktualisiert wird.

2.7. Nutzung und Anbindung einer zweiten Datenbank

Einige Anwendungsfälle machen es notwendig, eine zweite Datenbank zu nutzen. Das ist beispielsweise notwendig, wenn Daten aus einem Altsystem über die Datenbank für andere Systeme bereitgestellt werden und diese Daten in eine IsyFact-Anwendung über einen Batch importiert werden sollen. Der Batch muss dann sowohl auf die Datenbank der IsyFact-Anwendung, als auch auf die Datenbank des Altsystems zugreifen.

Die Anbindung einer zweiten Datenbank erfolgt analog zur Anbindung der primären Datenbank über Spring und die Nutzung über JPA. Dabei erfolgt der Zugriff auf die zweite Datenbank getrennt über einen weiteren Entity Manager und eine weitere Data Source.

2.8. 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.

2.8.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.

2.9. 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.

3. Vorgaben zur Definition des O/R-Mappings

Die folgenden Vorgaben sind bei der Definition des O/R-Mappings zu beachten.

3.1. Nutzung von Annotationen

Die Definition des Mappings wird über Annotationen in den Entitäten durchgeführt. Pro Klasse wird über die Annotationen definiert, auf welche Tabelle sie abgebildet werden und wie ihre Variablen auf Datenbank-Felder abgebildet werden. Beispiele für Annotationen finden sich in den Beispielen auf dieser Seite.

Über Annotationen können einige wenige Mappings nicht definiert werden, welche über eine XML-Konfigurationsdatei definierbar sind. Ein Beispiel dafür ist das Mapping einer Klasse auf zwei verschiedene Tabellen.

Falls eine XML-Mapping-Konfiguration für eine Klasse notwendig ist, ist die Konfiguration für diese Klasse in einer XML-Konfigurationsdatei abzulegen. Diese wird automatisch von JPA verwendet.

3.2. Konfiguration der ID und Sequenz

Primärschlüssel werden in JPA mit den Annotationen @Id und @GeneratedValue markiert. Der GenerationType der @GeneratedValue Annotation muss in jedem Fall AUTO sein. Es muss unbedingt darauf geachtet werden, das Inkrement (INCREMENT BY) der zur ID-Generierung genutzt Datenbanksequenz auf denselben Wert einzustellen, der auch beim SequenceGenerator im Parameter allocationSize angegeben ist.

3.3. 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 ID-Spalten 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.

3.4. Definition von 1-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.

3.5. Bidirektionale Assoziationen vermeiden

Bidirektional traversierbare Assoziationen sind zu vermeiden. Für die Traversierung in Gegenrichtung sollte eine Query verwendet werden.

Grund für die Vorgabe ist, dass Änderungen am "inversen Ende" der Assoziation nicht persistiert werden. Falls wirklich eine bidirektionale Assoziation benötigt wird, sind in der Entität am "inversen Ende" der Assoziation Methoden zu definieren, welche die Assoziation korrekt manipulieren.

Explizit verboten sind bidirektional traversierbare n-zu-m-Assoziationen. Hierfür sind zwei 1-zu-n- (bzw. n-zu-1-) Assoziationen zu definieren.

3.6. Vererbungshierarchien

Die JPA-Spezifikation beschreibt mehrere Strategien des O/R-Mappings von Vererbungshierarchien, zu beachten ist auch Vererbung im Persistenz-Klassenmodell.

Es werden zunächst die Architekturregeln der IsyFact für die vier O/R-Mapping-Strategien vorgestellt.

3.6.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.

Architekturregel

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.

3.6.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.

Architekturregel

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.

3.6.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.

Architekturregel

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.

3.6.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.

Architekturregel

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.

3.6.5. Beispiele, Vor- und Nachteile

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

3.6.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 3. 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 4. Polymorpher Zugriff
List<Person> personen = entityManager
    .createQuery("select p from Person p")
    .getResultList();

//oder per Spring Data Repository:
@Autowired
private PersonRepository personRepository;
List<Person> personen = personRepository.findAll();

//Zugriff auf Attribute 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.

3.6.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 5. 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.

3.6.5.3. Table per Concrete Class

Die Oberklasse wird folgendermaßen annotiert:

Listing 6. 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.

3.6.5.4. Mapped Superclass

Die Oberklasse wird folgendermaßen annotiert:

Listing 7. 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.

Zur Vertiefung bieten sich die folgenden Quellen an:

3.7. 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.

3.8. 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.

3.9. 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 und als Integer abgelegt. Diese Ablage ist sehr ungünstig, weil sich beim Hinzufügen oder Entfernen einer Enum-Ausprägung, die nicht die letzte ist, die Nummern verschieben und dadurch eine Datenmigration erforderlich wird.

STRING

Es wird der Java-Name der Enum-Ausprägung in der Datenbank abgelegt. Diese Ablage ist problematisch, weil sie eine enge Kopplung des Java-Quellcodes an die Datenbankinhalte erzeugt. 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 die Bibliothek isy-persistence zwei Hibernate User-Types zur Verfügung, um Enum-Werte auf eine Zeichenkette in der Datenbank abzubilden. Die Verwendung dieser User-Types beschreibt das Nutzungskonzept.

3.10. 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 8. 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 9. Konfiguration zur Abschaltung der automatischen Erzeugung
spring.jpa.hibernate.ddl-auto=none

Auch eine Validierung des Datenbankschemas durch Setzen des Parameters auf validate findet nicht statt. Stattdessen wird eine explizite Versionierung des Schemas verwendet.

3.11. 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).

4. Konventionen zur Erstellung von Datenbankschemas

Für die Erstellung von Datenbankschemas ist folgende Konvention zu beachten.

Der Name eines Datenbankschemas genügt den folgenden Anforderungen:

  • er enthält vollständige, beschreibende, aussprechbare Namen (oder bekannte Abkürzungen),

  • er muss mit einem Buchstaben beginnen,

  • nur Buchstaben, Zahlen und Unterstriche (_) sind erlaubt,

  • Umlaute, Sonderzeichen, Bindestriche und Leerzeichen sind nicht erlaubt.