Vor- und Nachteile von Hibernate 4

Hibernate 4: Was ist neu?

Michael Plöd

Nach nunmehr sieben Jahren hat das Hibernate-Projekt mit Version 4 ein neues Major Release veröffentlicht. Fokus dieser Version liegt auf einem internen Umbau, Aufräumarbeiten und einer kleinen Anzahl neuer, aber dennoch sehr praktischer Features. Dieser Artikel stellt Hibernate 4 auf Basis des aktuell vorliegenden Point-Release 4.1 vor und vermittelt Ihnen Tipps und Tricks für die Migration auf die neue Version.

Mit Sicherheit kann man feststellen, dass das Hibernate-Team in den eben angesprochenen sieben Jahren alles andere als untätig war. So entstanden in dieser Zeit zahlreiche Zusatzmodule wie die Lucene-Integration Hibernate Search, die Unterstützung für Sharding mit Hibernate Shards, die Referenzimplementierung für Bean Validation mit Hibernate Validator oder das Versionierungsmodul Envers. Im Bereich des Kerns von Hibernate fanden allerdings eher konservative Änderungen statt, welche man als „Produktpflege“ bezeichnen kann. An dieser Stelle setzt Hibernate 4 an, indem zahlreiche Interna aufgeräumt und einige neue Features implementiert wurden. Zu den prominenten neuen Funktionen zählt die Unterstützung von Mandantenfähigkeit („Multi-Tenancy Support“) und das in Version 4.1 hinzugekommene API für das Laden einer Entität über natürliche Schlüssel. Eine weitere Neuerung stellt die Service Registry dar, die es Entwicklern ermöglicht, Standarddienste wie zum Beispiel das Handling von JDBC Connections oder von Transaktionen zu ändern oder gänzlich neu zu implementieren. Des Weiteren besteht die Möglichkeit, eigene Dienste in der Service Registry anzumelden.

Zu den internen Änderungen zählt die Trennung zwischen API- und Implementierungsklassen, das Aufräumen veralteter Methoden und Klassen sowie verbessertes Logging mit Unterstützung für Internationalisierung und Message-Codes. Des Weiteren wurde das in Version 3.x optionale Modul Hibernate Annotations in den Kern von Hibernate überführt, sodass diese Abhängigkeit künftig nicht mehr explizit gepflegt werden muss. In diesem Zug wurde mit Version 4.1 der Hibernate-Kern von hibernate-core auf hibernate-orm umbenannt. Dies ist künftig auf beim Pflegen von Abhängigkeiten mit Maven zu berücksichtigen.

Mandantenunterstützung

Anwendungen, die mehrere Mandanten unterstützen müssen, bewerkstelligen dies in der Regel auf drei unterschiedliche Herangehensweisen. So besteht die Möglichkeit, jeden Mandanten in einer eigenen physischen Datenbankinstanz zu beherbergen. Die nächste Option sind getrennte Schemata innerhalb einer einzigen physischen Datenbankinstanz. Abschließend gibt es noch partitionierte Daten als letzte Strategie. Bei dieser werden alle Daten der Mandanten innerhalb einer Instanz und einem Schema in den gleichen Tabellen vorgehalten und anhand einer Diskriminator-Spalte voneinander getrennt. Unterstützung für letztere Art von Mandantenfähigkeit besteht bereits seit Hibernate 3.x durch Hibernate Shards oder die Verwendung von Filtern im Kern von Hibernate.

Für die ersten beiden Ansätze gab es bis dato allerdings nur die Möglichkeit für jedes Schema oder für jede Datenbank eine eigene SessionFactory zu verwenden. Insbesondere bei großenSessionFactories hat diese Herangehensweise zwei entscheidende Nachteile: Jede SessionFactory wird einzeln gestartet, was zu einer Multiplikation der Ramp-up-Zeit der Anwendung führt und jede SessionFactory benötigt Speicher, der ebenfalls pro Factory anfällt. Weiterhin besteht die Möglichkeit, einen ConnectionProvider zu verwenden, der dafür sorgt, dass die für den Mandanten benötigte Verbindung zur Datenbank an die Session gebunden wird. Allerdings hat dieser Ansatz den Nachteil, dass er nicht mit dem Einsatz eines Second Level Caches kompatibel ist.

In Hibernate 4 wurde die Unterstützung für Mandantenfähigkeit sowohl auf Ebene der Konfiguration also auch in dem API nativ vorgesehen. In der Konfiguration wird zuerst mithilfe des Attributshibernate.multiTenacy angegeben, ob mit der Schema-, Datenbankinstanz- oder Diskriminator-Strategie gearbeitet werden soll. Die Ausprägungen wären wie folgt:

  • SCHEMA
  • DATABASE
  • DISCRIMINATOR

Wird die Mandantenunterstützung mit Schema- oder Datenbankinstanz-Trennung implementiert, muss auch noch ein entsprechender ConnectionProvider implementiert werden, der dafür Sorge trägt, dass die für den Mandanten korrekte Verbindung zur Datenbank zur Verwendung kommt. Dafür müssen wir als Entwickler eine Implementierung des Service Provider Interfacesorg.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider bereitstellen. Dieser Provider kann entweder über den Konfigurationsparameter hibernate.multi_tenant_connection_provideroder über den ServiceRegistryBuilder in Hibernate eingestellt werden. Die Beispielimplementierung in Listing 1 veranschaulicht die Verwendung des ConnectionProviders.

public class MultiTenantConnectionProviderImpl extends AbstractMultiTenantConnectionProvider {
private final ConnectionProvider bank1 = ConnectionProviderBuilder.buildConnectionProvider("bank1DataSource");
private final ConnectionProvider bank2 = ConnectionProviderBuilder.buildConnectionProvider("bank2DataSource");

@Override
protected ConnectionProvider selectConnectionProvider(String mandant) {
if ( "bank1".equals(mandant) ) {
return bank1;
} else if ( "bank2".equals(mandant) ) {
return bank2;
} else {
throw new IllegalArgumentException( "Unbekannter Mandant" );
}
}

@Override
protected ConnectionProvider getAnyConnectionProvider() {
return bank1;
}
}

In der Anwendung selbst müssen wir als Entwickler nun nur noch an die für den jeweiligen Mandanten korrekte Session kommen. Verwaltet man seine Sessions selbst übersessionFactory.openSession()so muss nun folgender Code zum Einsatz kommen:

Session session = sessionFactory.withOptions()
.tenantIdentifier(mandant)
.openSession();

Inzwischen dürften jedoch die meisten Anwendungen ihre Session über sessionFactory.getCurrentSession()beziehen. In diesem Fall müssen wir noch einen Resolver bereitstellen. Dieser Resolver ist eine Implementierung des Service Provider Interfaces CurrentTenantIdentifierResolver. Die Implementierung wird schließlich über die Einstellung hibernate.tenant_identifier_resolver in der Konfiguration festgelegt.

Hervorzuheben ist hinsichtlich des in Hibernate 4 hinzugekommenen Mandantensupports, dass dieser voll kompatibel mit dem Einsatz von Second Level Caching ist, was bei den Lösungen auf Basis von Hibernate 3.x noch nicht der Fall war.

[ header = Seite 2: Laden über natürliche Schlüssel ]

Laden über natürliche Schlüssel

Das Laden über natürliche Schlüssel, die mit @NaturalId gemappt sind, war bis zur Version 4.1 von Hibernate nur über Criteria Queries möglich. Hibernate 4.1 führt hierfür ein API ein. Neu hinzugekommen sind primär die Methode session.byNaturalId(..),die einen NaturalIdLoadAccess zurückgibt, auf dem man dann mithilfe der Methode using(…)die einzelnen Attribute spezifizieren kann. Hat eine Entität nur ein einziges Feld mit einem natürlichen Schlüssel, so kann mit session.bySimpleNatrualId(…)gearbeitet werden. Im Unterschied zu dem Laden von Entitäten über natürliche Schlüssel mithilfe von Criteria sieht das Hibernate 4 API über by(Simple)NatrualId auch Unterstützung für die Semantiken von load(..),wir laden sofort, und getReference(),wir erstellen einen Proxy und laden lazy bei Zugriff auf den Proxy, vor. Betrachten wir ein einfaches Beispiel indem wir eine Kreditkarte auf Basis ihrer Nummer, die den natürlichen Schlüssel bildet, laden:

@Entity
public class CreditCard {
@Id
private long id;

@NaturalId
private String number;
...
}

Die einfachste Variante, eine Kreditkarte über ihre Nummer mithilfe des neuen API zu laden, ist die Verwendung von bySimpleNaturalId().Dies ist möglich, weil die Entität nur ein Attribut besitzt, das mit @NaturalId gemappt ist:

session.bySimpleNaturalId(CreditCard.class).load(1234123412341234);

Alternativ besteht die Möglichkeit, einzelne Attribute mithilfe von byNaturalId(..)zu adressieren. Bitte beachten Sie weiterhin den Einsatz von getReference(..)anstelle von load(..):

session.byNaturalId(CreditCard.class).using(„number“, 1234123412341234).getReference();

Services und die Service Registry

Services sind in Hibernate Interfaces und deren Implementierung, die bestimmte Funktionen bereitstellen. Als Entwickler haben wir die Möglichkeit das Verhalten dieser Services zu verändern, indem wir die Standardimplementierung mithilfe von Vererbung überschreiben. Des Weiteren kann die bestehende Menge von Diensten um neue erweitert werden.

In Hibernate 4 werden die eben genannten Services durch die Service Registry verwaltet. Die Service Registry ist außerdem die zentrale Anlaufstelle für den Zugriff und das Registrieren von Services. Von den Standardservices, die mit Hibernate ausgeliefert werden, haben wir bei der Einführung der Mandantenfähigkeit schon den MultiTenantConnectionProvider kennengelernt. Weitere Services sind u. a.: Dienste für die JTA-Implementierungen einzelner Applikationsserver, der JMX-Dienst, die Konfiguration von Hibernate, das Auflösen von SQL-Dialekten oder ein JNDI-Dienst, der JNDI-spezifische Funktionen für Hibernate bereitstellt. Neben der BootstrapServiceRegistry, die das minimale Set an Services enthält, gibt es noch das Konzept der Integrators. Diese sind eine neue Möglichkeit um das Hochfahren der SessionFactory zu beeinflussen. Ein eigener Integrator muss das Interface org.hibernate.integrator.spi.Integrator implementieren. Über die Methodeintegrate()kann man sich in den Prozess des Hochfahrens einklinken, wohingegen disintegrate()eine Beeinflussung des Herunterfahrens der SessionFactory ermöglicht.

[ header = Seite 3: Migration auf Hibernate 4 ]

Migration auf Hibernate 4

Will man von Hibernate 3 auf die neueste Version migrieren, gilt es einige Aspekte zu beachten, die in der Dokumentation von Hibernate noch nicht ganz transparent dargestellt sind. Diese möchte ich hier kurz zusammenfassen. Im Rahmen der Migration auf Services haben sich einige Werte von Konfigurationsparametern geändert. So wurden zum Beispiel die Provider für den Second Level Cache umgezogen, als Service implementiert und umbenannt. Der Konfigurationsparameter hibernate.cache.provider_class in hibernate.cache.region.factory_class wurde umbenannt und die Ausprägung änderte sich von org.hibernate.cache.EhCacheProvider auf org.hibernate.cache.ehcache.EhCacheRegionFactory .

Weiterhin sollte man im Umfeld von Caches die Benennungen der Default Cache Regions in der jeweiligen Cache Konfiguration prüfen. Aus org.hibernate.cache.StandardQueryCache wirdorg.hibernate.cache.internal.StandardQueryCache und aus org.hibernate.cache.UpdateTimestampsCache wird org.hibernate.cache.spi.UpdateTimestampsCache .

Listener können nicht mehr über die Konfiguration hibernate.cfg.xml registriert werden. Diese sind künftig über die eben erwähnten Integrators an der SessionFactory zu registrieren.

Falls Sie UserTypes verwenden, sollten Sie die Methoden nullSafeGet und nullSafeSet auf die neue Signatur umziehen. Des Weiteren wurde session.connection()abgelöst. Für Arbeiten auf nativer JDBC-Ebene stehen Ihnen in Zukunft session.doWork(..) und session.doReturningWork(..)zur Verfügung.

Bei dem Einsatz von nativen SQL-Abfragen mit Skalaren sollte man noch darauf achten, dass sich die Spezifikation des Typs der Skalar-Spalten geändert hat. Die Hibernate-3.x-kompatible Abfrage in Listing 2 kann in Version 4 aufgrund der Verwendung von Hibernate.STRING in addScalar(..)nicht mehr verwendet werden.

List<Object[]> list = getSession()
.createSQLQuery("select article_type, count(*) from Articles" +
"group by article_id " +
"order by count desc "
)
.addScalar("article_id", Hibernate.STRING)
.addScalar("count", Hibernate.LONG)
.list();

Die korrekte Hibernate-4-Anwendung sieht aus wie in Listing 3.

List<Object[]> list = getSession()
.createSQLQuery("select article_type, count(*) from Articles" +
"group by article_id " +
"order by count desc "
)
.addScalar("article_id", StringType.INSTANCE)
.addScalar("count", LongType.INSTANCE)
.list();

Fazit

Schlussendlich stellt sich für jedes Projekt natürlich die Frage: Wann soll man von Version 3 auf 4 updaten? Alleine auf Basis der neuen Features dürfte für die meisten Projekte vorerst kein Update-Bedarf bestehen. Eine Ausnahme hierzu bilden die Projekte, die Mandantenfähigkeit implementieren müssen. Das Konzept der Services mit der ServiceRegistry bietet sich für Projekte an, die das Verhalten von Hibernate mehr oder minder stark beeinflussen wollen. Für diese Projekte bietet Hibernate nun eine mächtige und vor allem saubere Möglichkeit der Einflussnahme. Rein qualitativ betrachtet konnte der Autor des Artikels seit nunmehr zwei Monaten im produktiven Einsatz keine Kritikpunkte finden. Stabilität und Performance bewegen sich im gewohnten und sehr guten Rahmen.

Einzig die Dokumentation mitsamt des Migration Guide verdienen noch Kritik. An dieser Stelle muss noch nachgebessert werden. Hier bewegen sich noch einige Bereiche in einem inkonsistenten bzw. undokumentierten Zustand. Das Problem ist dem Team jedoch bewusst und wurde auch offen kommuniziert.

Geschrieben von
Michael Plöd
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.