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.
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.
Qualitätskriterium | Vorteile |
---|---|
Leistungsfähigkeit (Performance Efficiency) |
IT-Systeme können schneller antworten und dabei Ressourcen schonen, da der Cache Datenbankzugriffe einspart. |
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.
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.
Qualitätskriterium | Empfehlungen |
---|---|
Leistungsfähigkeit (Performance Efficiency) |
Nur so viel wie nötig an Daten im Cache halten. |
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.
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.)
@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;
}
}
//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.
|
@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.
@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.
Die folgenden Best Practices helfen bei der Verwendung von Assoziationen: |
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.
@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.
@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.
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.
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.
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.
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.
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.
@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;
}
//…
}
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, istklassenstufe
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:
@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:
@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:
@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: |
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 mittelscompareTo
. -
Ein Vergleich mit
compareTo
muss immer auf dem Attribut mit höherer Genauigkeit (also auf demjava.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.
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.
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).