Persistenz in Java: Neues seit Hibernate ORM 6, Teil 2

Seit dem großen 6.0-Release hat Hibernate zahlreiche nützliche Ergänzungen, unter anderem für die Abfragesprache Hiberante Query Language erhalten.

In Pocket speichern vorlesen Druckansicht 18 Kommentare lesen

(Bild: rawf8/Shutterstock.com)

Lesezeit: 9 Min.
Von
  • Thorben Janssen
Inhaltsverzeichnis

Nachdem das Release von Hibernate ORM 6.0 hauptsächlich interne Änderungen und aufgrund der Namensänderungen in JPA 3 (Jakarta Persistence API) auch Migrationsaufwand für bestehende Projekte gebracht hatte, hatten viele erwartet, dass es um das beliebte Persistenzframework ruhiger werden würde. Aber das Gegenteil war der Fall: Inzwischen liegt Hibernate ORM in der Version 6.4 vor, und es hat sich einiges getan.

Der erste Teil dieses zweiteiligen Artikels hat den Fokus auf das Anbinden von Records, die verbesserte Mandantenfähigkeit und das neue Soft Delete gelegt. Die bisherigen Hibernate-6-Releases haben darüber hinaus einiges zu bieten. Unter anderem haben sie den Funktionsumfang der Abfragesprache Hibernate Query Language (HQL) erweitert, eine Unterstützung für zusammengesetzte Spaltentypen eingeführt, das Verarbeiten zeitzonenbasierter Zeitstempel verbessert und das Zusammenspiel mit der Criteria-API vereinfacht.

Die durch den JPA-Standard definierte Abfragesprache JPQL (Java Persistence Query Language) und Hibernates Erweiterung HQL hatten nie den Anspruch, den vollen Funktionsumfang von SQL abzubilden. Viele wünschen sich dennoch mehr Abfragemöglichkeiten, um seltener native SQL Queries verwenden zu müssen. Mit Window-Funktionen, Mengenoperationen und einigen weiteren, kleineren Änderungen hat Hibernate 6 in dem Bereich einiges zu bieten.

Window-Funktionen sind ein mächtiges Feature in SQL, um Operationen auf Teilbereichen der Ergebnismenge einer Abfrage durchzuführen. Die Anwendungsmöglichkeiten sind vielseitig. Ein Beispiel ist eine Abfrage, die Mitarbeiter unterschiedlicher Abteilungen mit ihrem Gehalt selektiert und dabei das Durchschnittsgehalt der jeweiligen Abteilung berechnet und zurückgibt.

Ein Vorteil von Window-Funktionen ist, dass die Datenbank Berechnungen auf größeren Datenmengen meist deutlich effizienter durchführen kann als Anwendungscode. In Hibernate-basierten Persistenzschichten findet man Window-Funktionen bisher allerdings nur selten. Ein Grund dafür dürfte sein, dass sie bisher ausschließlich als native SQL-Abfragen laufen konnten.

Mit Version 6 bietet Hibernates Abfragesprache HQL eine native Anbindung an Window-Funktionen. Die Syntax ist an die aus SQL bekannte Funktionsweise angelehnt und detailliert in der Dokumentation beschrieben.

Folgender Code zeigt ein kurzes Beispiel einer Window-Funktion:

List<EmployeeInfo> emps = em.createQuery("""
  SELECT new com.thorben.janssen.EmployeeInfo(
    firstName, 
    lastName, 
    department, 
    salary, 
    avg(salary) 
    OVER (PARTITION BY department))
  FROM Employee e""", 
  EmployeeInfo.class)
  .getResultList();

Diese Abfrage selektiert aus der Tabelle Employee den Namen, die Abteilung und das Gehalt aller Mitarbeiterinnen und Mitarbeiter. Zusätzlich führt sie mit den Schlüsselwörtern OVER und PARTITION eine Window-Funktion aus, die das durchschnittliche Gehalt der Mitarbeiter der jeweiligen Abteilung berechnet. Die Datenbank liefert die in Abbildung 1 gezeigten Informationen.

Hibernate kann die Informationen aus der Datenbank auf das Objekt EmployeeInfo abbilden und zurückgeben (Abb. 1).

(Bild: Screenshot (Thorben Janssen))

Hibernate 5 konnte die Datentypen OffsetDateTime und ZonedDateTime nur eingeschränkt verarbeiten. Beim Speichern hat es den Zeitstempel in die Zeitzone der Anwendung konvertiert und anschließend ohne Zeitzoneninformationen gespeichert. Dieses Vorgehen erfordert, dass alle Anwendungsinstanzen dieselbe Zeitzone verwenden, und führt beim Wechsel zwischen Sommer- und Winterzeit zu Problemen.

Mehr Infos

(Bild: DOAG)

Die JavaLand-Konferenz findet dieses Jahr vom 9. bis 11. April erstmals am Nürburgring statt. Die Hauptkonferenz der Jubiläumsausgabe bietet rund 140 Vorträge zu den jüngsten und den kommenden Entwicklungen rund um Java und Jakarta EE. Daneben stehen der Einsatz von KI und das Zusammenspiel mit anderen Programmiersprachen auf der Agenda.

Der Autor dieses Artikels Thorben Janssen hält auf der Konferenz einen Vortrag zu den Neuerungen in Hibernate 6.

Die JavaLand-Veranstaltung ist eine Community-Konferenz für Java-Entwickler und wird durchgeführt von der Deutschen Oracle-Anwendergemeinschaft (DOAG) und Heise Medien in Zusammenarbeit mit dem iJUG, dem Interessenverbund deutschsprachiger Java User Groups.

Als Ausweg hat das Hibernate-Team in Version 6.0 den TimezoneStorageType eingeführt und mit Version 6.2 noch einmal verändert. Der von Hibernate zu verwendende TimezoneStorageType lässt sich über die Annotation @TimeZoneStorage für jede Entitätseigenschaft definieren oder mit dem Konfigurationsparameter hibernate.timezone.default_storage anwendungsweit festlegen:

@Entity
public class MyEntity {
    
  @TimeZoneStorage(TimeZoneStorageType.DEFAULT)
  private ZonedDateTime zonedDateTime;

  ...
}

Hibernate ORM 6.4 kennt folgende TimezoneStorageTypes:

  • NATIVE speichert den Zeitstempel in einer Datenbankspalte vom Typ timestamp with timezone, den nicht alle Datenbanken kennen.
  • NORMALIZE normalisiert den Zeitstempel in die Zeitzone der Anwendung. Das entspricht dem Verhalten von Hibernate 5.
  • NORMALIZE_UTC normalisiert den Zeitstempel in die Zeitzone UTC. Das entspricht der für Hibernate 5 empfohlenen Normalisierung.
  • COLUMN speichert den Zeitstempel in zwei Datenbankspalten. Eine enthält den nach UTC normalisierten Zeitstempel und die zweite die Differenz der Zeitzone zu UTC.
  • AUTO verwendet den durch den Datenbankdialekt vorgegebenen TimezoneStorageType: NATIVE für Datenbanken, die den Spaltentyp timestamp with timezone anbieten und COLUMN für alle anderen Datenbanken.
  • DEFAULT kam mit Hibernate 6.2 hinzu und verwendet TimezoneStorageType.NATIVE für Datenbanken, die ihn kennen, und für alle anderen Datenbanken NORMALIZE_UTC.

Moderne Datenbanken bieten mehr als nur die einfachen Spaltentypen der meisten Tabellenmodelle. Neben den ebenfalls weit verbreiteten JSON- und XML-Typen, kennen viele Datenbanken Composite Types, also zusammengesetzte Spaltentypen. Diese frei definierbaren Typen bestehen aus mehreren benannten, typisierten Feldern:

create type my_complex_type 
  as (aLong bigint, 
      aString varchar(255));
create table MyComplexEntity (
  id bigint not null,
  complexData my_complex_type,
  primary key (id)
)

Seit Version 6.2 kann Hibernate für Oracle, PostgreSQL und DB2 solche Spaltentypen auf Embeddables abbilden. Bei einem Embeddable handelt es sich um eine wiederverwendbare Komponente, die aus mehreren Eigenschaften und ihren Spaltenabbildungen besteht.

Das nachfolgende Beispiel implementiert das Embeddable als Java-Klasse. Wie im ersten Teil dieses zweiteiligen Artikels gezeigt, erlaubt Hibernate 6 auch eine Implementierung als Record.

@Embeddable
@Struct(name = "my_complex_type")
public class MyComplexType {

  private String aString;

  private Long aLong;

  ...
}

Mit der neu eingeführten Annotation @Struct kann Hibernate das Embeddable auf eine Tabellenspalte mit dem referenzierten Typ abbilden. Dabei bildet es jede Eigenschaft des Embeddable auf ein Feld des zusammengesetzten Typs ab. Der Name des jeweiligen Feldes entspricht dem Namen der Entitätseigenschaft, sofern er nicht über die Annotation @Column definiert ist.

Die Abbildung des Embeddable auf einen zusammengesetzten Spaltentyp hat keine Auswirkungen auf die Definition oder Verwendung einer Entität. Mit der Annotation @Embedded versehen verarbeitet Hibernate das Embeddable auf die übliche Weise:

@Entity
public class MyEntity {
  @Embedded
  private MyComplexType complexData;

  ...
}

Embeddables lassen sich auch in Abfragen verwenden. Der Pfadoperator . dient dazu, von der Entität zum Embeddable und von dort zu dessen Eigenschaften zu navigieren.

MyEntity e = em.createQuery("""
  SELECT e 
  FROM MyEntity e 
  WHERE e.complexData.aLong = 456""", 
                             MyEntity.class)
  .getSingleResult();

Während Hibernate die Abfrage generiert, bildet es die Eigenschaften des Embeddable auf die Felder des zusammengesetzten Typs ab.

16:06:43,917 DEBUG [org.hibernate.SQL] - 
  select
    m1_0.id,
    (m1_0.complexData).aLong,
    (m1_0.complexData).aString 
  from
    MyEntity m1_0 
  where
    (
      m1_0.complexData
    ).aLong=456

Mit der Criteria-API bietet die JPA-Spezifikation seit langem eine Schnittstelle, um Abfragen dynamisch und typsicher zu erzeugen. Viele kritisieren die API jedoch als zu umständlich.

Mit Version 6.3 hat das Hibernate-Team zwei proprietäre Vereinfachungen eingeführt, um eine CriteriaQuery aus einem HQL-Statement zu erzeugen oder mit einer CriteriaDefinition zu definieren.

Ein CriteriaQuery-Objekt aus einer HQL-Abfrage zu erzeugen, ist einfach und bietet sich in den Fällen an, in denen ein Großteil der Abfrage statisch ist. Die Anwendung muss lediglich die Methode createQuery des HibernateCriteriaBuilder aufrufen und das HQL-Statement sowie den gewünschten Ergebnistyp der Abfrage übergeben.

Das Interface HibernateCriteriaBuilder erweitert das durch die JPA-Spezifikation definierte CriteriaBuilder-Interface. Die Instanz erhält man durch Aufruf der Methode getCriteriaBuilder auf einer Hibernate-Session. Die Methode unterscheidet sich lediglich durch ihren Rückgabetyp von der durch das Interface EntityManager definierten Methode getCriteriaBuilder. Somit kann ein Wechsel von dem in der JPA häufig verwendeten CriteriaBuilder auf die Hibernate-spezifische Erweiterungen in der Regel ohne größere Anpassungen erfolgen.

HibernateCriteriaBuilder builder = 
  em.unwrap(Session.class).getCriteriaBuilder();
JpaCriteriaQuery<Book> criteriaQuery = 
  builder.createQuery("SELECT b FROM Book b", Book.class);
Root<?> bookRoot = criteriaQuery.getRootList().get(0);

criteriaQuery.where(builder.like(bookRoot.get(Book_.TITLE),
                    "Hibernate %"));
Book book = em.createQuery(criteriaQuery).getSingleResult();

Nachdem eine Anwendung das CriteriaQuery-Objekt aus der HQL-Abfrage erstellt hat, kann sie die Abfrage mithilfe der Criteria-API anpassen, ausbauen und schließlich ausführen.

Das neue Interface CriteriaDefinition bietet eine nützliche Vereinfachung, um die vollständige Abfrage dynamisch über die Criteria-API zu erzeugen.

Book book = new CriteriaDefinition<>(em, Book.class) 
{{
  var book = from(Book.class);
  where(like(book.get(Book_.TITLE), "Hibernate %"));
}}
  .createQuery(em)
  .getSingleResult();

Das Interface stellt einen instanziierten CriteriaBuilder zur Verfügung und erstellt das CriteriaQuery-Objekt sowie einen Großteil der sich wiederholenden Methodenaufrufe, die für das Zusammenspiel mit der klassischen Criteria-API erforderlich sind. Damit können sich Entwicklerinnen und Entwickler darauf konzentrieren, die Abfrage zu erstellen.

Im obigen Beispiel müssen sie nur noch die Klauseln FROM und WHERE definieren und anschließend die Abfrage ausführen.