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.
- Thorben Janssen
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.
UnterstĂĽtzung fĂĽr Java Records
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.
Records als Embeddable
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.
Embeddable Records in Hibernate 6.0 bis 6.1
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;
...
}
Embeddable Records ab Hibernate 6.2
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.
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.
Records als Abfrageergebnisse
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.