zurück zum Artikel

Persistenz in Java: Neues seit Hibernate ORM 6

Thorben Janssen

(Bild: rawf8/Shutterstock.com)

Seit dem großen 6.0-Release hat sich bei Hibernate dank Embeddables der Umgang mit Java Records stark verbessert. Und es gibt weitere nützliche Ergänzungen.

Vor einiger Zeit hat Hibernate ORM 6.0 viel Aufmerksamkeit erhalten, weil die Migration einige Anpassungen wegen inkompatibler Änderungen der Jakarta Persistence API (JPA) 3.0 erforderte. Wer erwartet hatte, dass es danach wieder ruhiger um das beliebte Persistenz-Framework wurde, liegt falsch. Seitdem hat das Hibernate-Team eine Vielzahl an Verbesserungen veröffentlicht. Inzwischen liegt die Version 6.4 des Frameworks vor, und es ist an der Zeit, sich die wichtigsten Verbesserungen genauer anzuschauen.

Seit der Einführung von Records als Sprachfeature in Java [1], fragen sich viele Entwicklerinnen und Entwickler, wie sie Records mit Hibernate verwenden können. Für unveränderliche Datentypen scheint es eine Vielzahl von Anwendungsfällen zu geben. Die offensichtlichsten sind Abfrageergebnisse, unveränderliche Entitätseigenschaften oder sogar vollständig unveränderliche Entitäten.

Leider lässt es die JPA-Spezifikation nicht zu, Records als Entitäten zu verwenden . Laut Spezifikation muss eine Entitätsklasse eine nicht finale Klasse sei, die über einen parameterlosen Konstruktor verfügt und der Bean-Spezifikationen entspricht. Das bedeutet vereinfacht, dass die Entitätsklasse Getter- und Setter-Methoden für alle Eigenschaften bereitstellen soll. Da das bei einem Record nicht gegeben ist, lässt er sich nicht verwenden, um eine unveränderliche Entität zu modellieren.

Bei einem Embeddable handelt es sich um eine Klasse, die als wiederverwendbare Komponente mehrere Eigenschaften mit den zugehörigen Spaltenabbildungen umfasst. Im Gegensatz zu einer Entität verfügt ein Embeddable jedoch über keinen eigenen Lifecycle und kann ausschließlich als Eigenschaftstyp innerhalb einer Entität verwendet werden.

Laut JPA-Spezifikation ist es für Embeddables ähnlich wie für Entitäten nicht erlaubt, sie als Records zu implementieren. In älteren Versionen galt diese Einschränkung auch für Hibernate. Das Hauptproblem lag darin, dass ein Record nicht über den von Hibernate erwarteten, parameterlosen Konstruktor verfügt.

Seit Hibernate ORM 6.0 lässt sich das Problem mit einem EmbeddableInstantiator lösen:

public class AddressInstantiator 
  implements EmbeddableInstantiator 
{

  Logger log = 
   LogManager.getLogger(this.getClass().getName());

  public boolean 
    isInstance(Object object, 
               SessionFactoryImplementor sessionFactory) 
  {
    return object instanceof Address;
  }

  public boolean 
    isSameClass(Object object, 
                SessionFactoryImplementor sessionFactory) 
  {
    return object.getClass().equals( Address.class );
  }

  public Object 
    instantiate(ValueAccess valuesAccess, 
                SessionFactoryImplementor sessionFactory) 
  {
    // valuesAccess enthält die Eigenschaftswerte 
    // in alphabetischer Reihenfolge!
    final String city = 
      valuesAccess.getValue(0, String.class);
    final String postalCode = 
      valuesAccess.getValue(1, String.class);
    final String street = 
      valuesAccess.getValue(2, String.class);
    log.info("Instanziiere Address Embeddable für"
             +street+" "+postalCode+" "+city);
    return new Address( street, city, postalCode );
  }

}

Mit der Implementierung dieses einfachen Interfaces übernimmt der Entwickler oder die Entwicklerin die Kontrolle über die Instanziierung des Embeddable.

Hibernate ruft dazu die Methode instantiate mit einem Supplier, der alle verfügbaren Eigenschaftswerte des zu erzeugenden Embeddable enthält, und einer Instanz des SessionFactoryImplementor auf. Innerhalb der Methode lassen sich ein beliebiger Konstruktor und weitere Methoden aufrufen, um das Embeddable zu erzeugen.

Dabei ist zu beachten, dass der Supplier Zugriff auf die Eigenschaftswerte nur in alphabetischer Reihenfolge der Eigenschaftsnamen bietet. Um die Lesbarkeit und Wartbarkeit der EmbeddableInstantiator-Implementierung zu verbessern, empfiehlt es sich daher, vor dem Aufruf des Konstruktors die Werte in sinnvoll benannten, lokalen Variablen abzulegen.

Im nächsten Schritt gilt es, den EmbeddableInstantiator mit dem jeweiligen Embeddable zu verknüpfen. Das kann mithilfe der Annotation @EmbeddableInstantiator anwendungsweit für alle Instanzen eines Embeddable oder für jede Instanz individuell erfolgen. Das folgende Beispiel zeigt die anwendungsweite Registrierung des AddressInstantiator für das Embeddable Address.

@Embeddable
@EmbeddableInstantiator(AddressInstantiator.class)
public record Address (String street, String city,
                       String postalCode) {}

Im Anschluss lässt sich Address wie ein als Klasse implementiertes Embeddable verwenden. Dabei ist lediglich zu beachten, dass ein Record unveränderlich ist. Somit kann eine Anwendung die durch das Embeddable abgebildeten Werte nur ändern, indem sie das Record ersetzt.

@Entity public class Author {

  @Id
  @GeneratedValue
  private Long id;
   
  @Embedded
  private Address address;

  private String firstName;
  private String lastName;
  ...
}

Mit der Version 6.2 hat das Hibernate-Team die Implementierung von Embeddables als Records noch einmal deutlich vereinfacht. Da ein Record nur über einen Konstruktor verfügen kann, ermittelt Hibernate ORM den Konstruktor und verwendet ihn, um das Embeddable zu erzeugen. Es ist somit nicht mehr erforderlich, einen EmbeddableInstantiator zu implementieren und zu referenzieren.

Hibernate auf der Community-Konferenz für Java-Entwickler

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

Das Interface EmbeddableInstantiator steht weiterhin zur Verfügung. Man kann es unter anderem verwenden, um das Erstellen eines als Java-Klasse implementierten Embeddable an die eigenen Bedürfnisse anzupassen. Hierbei unterscheidet sich das Vorgehen grundsätzlich nicht von der gezeigten Implementierung eines EmbeddableInstantiator zum Erzeugen eines Record.

JPA und Hibernate konnten Records seit deren Einführung für Abfrageergebnisse verwenden. Hierzu kann wie beim Abbilden auf einfache Java-Objekte eine Konstruktorreferenz dienen [3].

List<BookPublisherRecord> bookPublisherValues = 
  em.createQuery("SELECT new com.thorben.janssen" +
                 ".hibernate.performance.model." +
                 "BookPublisherRecord(b.title, "+
                 "b.publisher.name) FROM Book b", 
                 BookPublisherRecord.class).getResultList();

Für dieses Feature gab es allerdings auch viel Kritik, weil die Lesbarkeit der Abfrage unter dem vollreferenzierten Klassennamen leidet. Darüber hinaus ist die Auflistung aller Konstruktorparameter bei großen Objekten schlecht wartbar.

Eine Neuerung in Hibernate 6 verbessert zumindest das Problem der Lesbarkeit. Nun reicht es aus, den Record als Rückgabetyp der Abfrage zu benennen und die Werte in der Reihenfolge zu selektieren, wie sie an dessen Konstruktor übergeben werden sollen.

List<BookPublisherRecord> bookPublisherValues = 
  em.createQuery("SELECT b.title, " +
                 "b.publisher.name FROM Book b", 
      BookPublisherRecord.class).getResultList();

Auch wenn diese Abfrage besser lesbar ist, bleiben die Wartbarkeitsprobleme gerade bei der Instanziierung großer Records weiterhin bestehen. Daher ist vor allem bei Records Vorsicht geboten, deren Konstruktor mehrere, aufeinanderfolgende Parameter desselben Typs erwartet. Hier besteht ein hohes Fehlerrisiko, da sich eine falsche Parameterreihenfolge erst beim Überprüfen eines erzeugten Records zeigt.

Die Möglichkeit, mit einem gemeinsamen Backend mehrere voneinander getrennte Mandanten zu bedienen, ist für viele Anwendungen relevant. In Version 5 war Hibernate ORM nur dazu in der Lage, wenn die Daten der Mandanten in voneinander getrennten Datenbanken oder Datenbankschemata vorlagen. Seit Version 6 ist der als "partitioned data" bezeichnete Einsatz einer zusätzlichen Tabellenspalte zum Zuordnen der Mandanten möglich.

Hierzu muss man die Entitäten um eine zusätzliche, mit @TenantId annotierte Eigenschaft erweitern.

@Entity
public class Book {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  private String title;

  @TenantId
  private String tenant;

  ...
}

Um die Mandanten-ID zu erhalten, erwartet Hibernate wie bereits in Version 5 eine Implementierung von CurrentTenantIdentifierResolver. Gängige Implementierungen dieses Interfaces fragen die Mandanten-ID von der Authentifizierungskomponente ab und stellen sie Hibernate zur Verfügung. In der Vergangenheit erlaubte Hibernate ausschließlich Strings als Mandanten-IDs, aber seit Version 6.4 [4] auch andere Eigenschaftstypen. Üblicherweise verwenden Anwendungen die Typen String, Integer oder Long.

Hibernate setzt die ID automatisch beim Persistieren einer neuen Entität und erweitert die ausgeführten Abfragen, um die Ergebnisse auf den aktuell aktiven Mandaten einzuschränken.

em.createQuery("SELECT b FROM Book b " + 
               "WHERE b.title = :title", Book.class)
  .setParameter("title", "My Book")
  .getResultList();

09:00:15,976 DEBUG [org.hibernate.SQL] - 
    select
        b1_0.id,
        b1_0.tenant,
        b1_0.title,
        b1_0.version 
    from
        Book b1_0 
    where
        b1_0.tenant = ? 
        and b1_0.title=?

Eine häufige Anforderung von Enterprise-Anwendungen sind Soft Deletes, um die Datensätze nicht zu löschen, sondern zu deaktivieren. Die Daten sind auf Anwenderseite nicht mehr sichtbar, stehen aber für weitere Auswertungen oder zur späteren Nachvollziehbarkeit weiterhin zur Verfügung.

In der Vergangenheit musste man dazu manuell einen Filter einsetzen und die Löschoperation überschreiben [5].

Seit der Version 6.4 bietet Hibernate die @SoftDelete-Annotation.

@Entity
@SoftDelete
public class Book { ... }

Für eine damit annotierte Entität speichert Hibernate den aktuellen Status des Datensatzes in einer zusätzlichen Datenbankspalte mit dem Typ Boolean. Die optionale Eigenschaft columnName der @SoftDelete-Annotation legt den Spaltennamen fest. Fehlt der Name, verwendet Hibernate die Spalte deleted.

Um inaktive Datensätze automatisch auszublenden, erweitert Hibernate alle Datenbankabfragen um eine zusätzliche Prüfung der Spalte deleted.

Book book = em.createQuery("SELECT b FROM Book b " + 
                           "WHERE b.title = :title", 
                           Book.class)
              .setParameter("title", "My Book")
              .getSingleResult();

09:15:13,799 DEBUG [org.hibernate.SQL] - 
    select
        b1_0.id,
        b1_0.title,
        b1_0.version 
    from
        Book b1_0 
    where
        b1_0.title=? 
        and b1_0.deleted=false

Solange der Datensatz aktiv ist, enthält deleted den Wert false. Beim Löschen der Entität führt Hibernate die Operation SQL UPDATE statt SQL DELETE aus und setzt den Wert von deleted auf true.

09:15:13,804 DEBUG [org.hibernate.SQL] - 
    update
        Book 
    set
        deleted=true 
    where
        id=? 
        and deleted=false 
        and version=?

Die Annotation @SoftDelete bietet zusätzliche Konfigurationsmöglichkeiten, um Hibernates Implementierung an Tabellenschemata anzupassen.

Intern basiert die Soft-Delete-Implementierung auf einem Boolean, der den aktuellen Status des jeweiligen Datensatzes abbildet. Wie Hibernate diesen Boolean auswertet, legt der Enum-Wert SoftDeleteType fest. Er ist Teil der Eigenschaft strategy von @SoftDelete und kann ACTIVE oder DELETED sein.

Im Standardfall verwendet Hibernate SoftDeleteType.DELETED. Dabei signalisiert true, dass der Datensatz deaktiviert ist. Bei der Strategie SoftDeleteType.ACTIVE bedeutet true hingegen, dass der Datensatz aktiv ist.

Wer den Status des Datensatzes nicht als Boolean in der Datenbank speichern möchte, kann das Vorgehen durch die Referenz eines AttributeConverter [6] anpassen. Dabei handelt es sich um ein einfaches Interface mit zwei Methoden, die die Umwandlung zwischen dem Typ der Entitätseigenschaft und der Datenbankspalte implementieren.

@Entity
@SoftDelete(strategy = SoftDeleteType.ACTIVE, 
            columnName = "status", 
            converter = SoftDeleteConverter.class)
public class Book { ... }
Java Flight Recorder Events

Seit Version 6.4 kann Hibernate ORM unterschiedliche interne Ereignisse als JFR-Events (Java Flight Recorder) ausgeben. Dazu gehören unter anderem das Ausführen von JDBC Statements, das Öffnen und Schließen einer Session, das Ausführen von Flush-Operationen und die Interaktionen mit dem 2nd Level Cache.

Um das Erzeugen von JFR-Events zu aktivieren, muss man die Komponente hibernate-jfr als Abhängigkeit zum Projekt hinzufügen und die Anwendung mit dem Flag --XX:StartFlightRecording:filename=<Dateiname>.jfr starten.

<dependency>
  <groupId>org.hibernate.orm</groupId>
  <artifactId>hibernate-jfr</artifactId>
  <version>${hibernate.version}</version>
</dependency>

Hibernate erzeugt daraufhin während der Ausführung der Anwendung eine Java-Flight-Recorder-Datei, die sich beispielsweise mit JDK Mission Control einlesen und auswerten lässt.

Das Tool JDK Mission Control hilft beim Auswerten der JFR-Datei (Abb. 1).

(Bild: Screenshot (Thorben Janssen))

Abgesehen von diesen Ergänzungen bringt Hibernate 6 eine verbesserte Behandlung von zeitzonenbehafteten Zeitstempeln und kann zusammengesetzte Spaltentypen abbilden. Darüber hinaus hat das Hibernate-Team den Funktionsumfang der Hibernate Query Language (HQL), Hibernates eigener JPQL-Erweiterung (Java Persistence Query Language) deutlich ausgebaut und als Hibernate-spezifische Erweiterung der Criteria-API bereitgestellt. Die Details wird der zweite Teil des Artikels behandeln.

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 [7] wöchentlich über Persistenzthemen und hilft Entwicklern im Persistence Hub [8], ihre Fertigkeiten zu verbessern.

(rme [9])


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

Links in diesem Artikel:
[1] https://www.heise.de/news/Java-15-versteckt-und-versiegelt-Klassen-4902566.html
[2] https://www.javaland.eu/de/home/
[3] https://thorben-janssen.com/dto-projections/#dto-projections-in-jpql
[4] https://hibernate.atlassian.net/browse/HHH-14822
[5] https://thorben-janssen.com/implement-soft-delete-hibernate/
[6] https://thorben-janssen.com/jpa-attribute-converter/
[7] https://thorben-janssen.com/
[8] https://thorben-janssen.com/join-persistence-hub/
[9] mailto:rme@ix.de