Persistenz in Java: Neues seit Hibernate ORM 6

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.

In Pocket speichern vorlesen Druckansicht 24 Kommentare lesen

(Bild: rawf8/Shutterstock.com)

Lesezeit: 9 Min.
Von
  • Thorben Janssen
Inhaltsverzeichnis

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

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.