zurück zum Artikel

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

Thorben Janssen

(Bild: rawf8/Shutterstock.com)

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

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 [1] 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 [2] 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:

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 [3], 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.

Beim 6.0-Release von Hibernate ORM lag der Schwerpunkt noch auf internen Verbesserungen und den durch die Jakarta Persistence API 3 eingebrachten Namensänderungen. Seitdem hat das Hibernate-Team viele Funktionen verbessert und das Zusammenspiel mit diversen APIs vereinfacht.

Hibernate hat die Abbildung komplexer Datentypen nach und nach ausgebaut. Zeitzonenbehaftete Zeitstempel lassen sich auf unterschiedliche Weise und ohne Normalisierung speichern. Das Team hat vor allem den Funktionsumfang von HQL, Hibernates eigener Erweiterung der standardisierten JPQL-Abfragesprache und die Criteria-API schrittweise erweitert. Inzwischen können Developer auf beiden Wegen deutlich komplexere Abfragen erstellen und müssen immer seltener auf native SQL-Abfragen ausweichen.

Es bleibt spannend zu beobachten, wie sich das Framework weiterentwickelt und das Erstellen komplexer Persistenzschichten immer weiter vereinfacht.

Thorben Janssen
löst als freiberuflicher Consultant und Trainer Persistenzprobleme mit JPA und Hibernate. Er ist Autor des Amazon Bestsellers "Hibernate Tips – More than 70 solutions to common Hibernate problems" und internationaler Redner mit 20 Jahren Erfahrung mit JPA und Hibernate. Auf thorben-janssen.com schreibt er [4] wöchentlich über Persistenzthemen und hilft Entwicklern im Persistence Hub [5] ihre Fertigkeiten zu verbessern.

(rme [6])


URL dieses Artikels:
https://www.heise.de/-9664620

Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Persistenz-in-Java-Neues-seit-Hibernate-ORM-6-9651063.html
[2] https://www.javaland.eu/de/home/
[3] https://www.heise.de/hintergrund/Persistenz-in-Java-Neues-seit-Hibernate-ORM-6-9651063.html
[4] https://thorben-janssen.com/
[5] https://thorben-janssen.com/join-persistence-hub/
[6] mailto:rme@ix.de