
|
HQL Hibernate Query Language: Eine auf Objektreferenzen basierende Abfragesprache, die dem Aufbau von SQL ähnelt. |
|
JPA Java Persistence API: Standard API für Persistenzschichten für Java J2SE und JEE Anwendungen. |
|
JPQL Java Persistence Query Language: Eine auf Objektreferenzen basierende Abfragesprache, die dem Aufbau von SQL ähnelt. |
|
Primary Key Eindeutiger Wert, der eine Datenbankzeile identifiziert. |
|
| Abb. 1: Synchronisation des Second Level Caches. Vergrößern |
public class SyncCacheListener implements PostCollectionUpdateEventListener,
PostInsertEventListener,
PostDeleteEventListener {
|
| Abb. 2: Der Entity Listener. |
final Object delegate = em.getDelegate(); Session session; |
| Abb. 3: Ermittlung der Hibernate SessionFactory. |
Die Messungen in den vorherigen Artikeln der Reihe [2] haben gezeigt, dass der Einsatz eines Second Level Caches die Performance von Hibernate positiv beeinflusst – die korrekte Anwendung und Konfiguration natürlich vorausgesetzt. Warum sollte also die Notwendigkeit bestehen, Einträge „manuell“ wieder aus dem Cache zu entfernen?
Das ist prinzipiell immer dann der Fall, wenn die zu Grunde liegenden Daten nicht nur über eine Hibernate Session, sondern auch auf anderem Wege manipuliert werden sollen. Dies kann im einfachsten Falle eine Änderung von Datensätzen über ein Datenbank-Tool oder einen Batchlauf bedeuten.
Ein etwas aufwendigeres Beispiel stellt ein Szenario dar, in dem zwei oder mehr unabhängige Applikationen auf denselben Datenbestand zugreifen sollen. In diesem Fall muss es eine Form der Cache-Synchronisation geben.
Das für diesen Artikel zu Grunde liegende Szenario lässt sich wie folgt beschreiben: Es existieren zwei unterschiedliche Web-Anwendungen, die teilweise auf demselben Datenbestand arbeiten. Die Persistenzschichten der Anwendungen sind mit unterschiedlichen Techniken realisiert, die einzige Gemeinsamkeit ist die Verwendung von Hibernate, wenn auch in verschiedenen Versionen.
Beide Anwendungen verwenden einen Second Level Cache. Dies führt zu dem Problem, dass Änderungen, die in Anwendung A vorgenommen wurden, nicht in Anwendung B sichtbar sind und umgekehrt. Aus diesem Grund soll eine Möglichkeit geschaffen werden, die Caches der beiden Applikationen zu synchronisieren. Hierfür wird im Falle einer Änderung am Datenbestand, die jeweils andere Anwendung darüber informiert und kann ihren Second Level Cache entsprechend aktualisieren.
Für die Kommunikation der beiden Anwendungen soll ein Modul zum Einsatz kommen, das mit Hilfe der Apache-Bibliothek HTTP-Client HTTP-Requests an das jeweils andere System absetzt (siehe Abbildung 1).
Der einfachste Ansatz, um die aus dem oben aufgeführten Szenario resultierenden Probleme zu umgehen, ist das Löschen des gesamten Caches in der jeweils anderen Hibernate Session. So könnte jede Datenmanipulation innerhalb einer der beiden Applikationen eine Benachrichtigung der anderen Anwendung auslösen, die daraufhin ihren Second Level Cache löscht.
Je nachdem wie häufig Datenänderungen stattfinden, kann dies jedoch auch dazu führen, dass der Performance-Vorteil des Second Level Caches durch das häufige Löschen verlorengeht oder gar ins Negative umgekehrt wird.
Es wäre also von Vorteil, wenn man nicht immer den gesamten Cache verwirft, sondern nur die Bereiche entfernt, die auch wirklich geändert worden sind. Doch wie kann möglichst einfach ermittelt werden, was sich eigentlich geändert hat?
In Hibernate 3 besteht die Möglichkeit, einen Listener zu verwenden, der informiert wird, sobald sich eine Entity oder eine Collection ändert. Ein solcher Listener kann über die Annotation @EntityListener an jede beliebige Entity angehängt werden:
@EntityListeners (SyncCacheListener.class)
Für verschiedene Aktionen, die die Entity betreffen, können verschiedene Methoden innerhalb des Listeners deklariert werden. Die dann auszuführenden Methoden werden hierfür mit einer entsprechenden Annotation (z. B. @PostUpdate, @PostPersist oder @PostRemove) gekennzeichnet. Der im Beispiel verwendete Listener implementiert zusätzliche Interfaces, so dass zum Beispiel auch auf Änderungen von Collection-Zuordnungen reagiert werden kann (siehe Abbildung 2).
Es besteht zwar die Möglichkeit, den Second Level Cache auch bei konkurrierenden Schreibzugriffen einzusetzen, um die Anzahl der Datenbankabfragen zu minimieren, dafür erzeugen die Benachrichtigungen untereinander jedoch zusätzlichen Netzwerkverkehr und benötigen Rechenzeit.
Um den Netzwerkverkehr der Anwendungen untereinander zu reduzieren, wurde auf beiden Seiten ein Service eingerichtet, der alle vorgenommenen Änderungen sammelt und nach einem konfigurierbaren Zeitintervall (z. B. alle 5 Sekunden) einen Request an das jeweils andere System absetzt. Anhand der Daten dieses Requests kann der Empfänger dann die geänderten Daten ermitteln und genau die Bestandteile aus seinem Cache entfernen, die davon betroffen sind.
Um Einträge im Second Level Cache überhaupt manipulieren zu können, wird die Hibernate Session bzw. SessionFactory benötigt. Diese kann bei Verwendung der JPA über die Methode getDelegate() des EntityManagers ermittelt werden. Aber Vorsicht: Der Rückgabetyp der Methode ist vom Typ Object und die konkrete Klasse kann je Aufruf unterschiedlich sein (siehe Abbildung 3).
Der Second Level Cache beinhaltet verschiedenartige Einträge:
Der Query Cache beinhaltet die Ergebnismengen bestimmter HQL- bzw. JPQL-Abfragen.
Liegt eine Abfrage im Query Cache, so wird die Ergebnismenge gespeichert und die Abfrage muss nicht jedes Mal neu durchgeführt werden. Das Hinzufügen von Queries in den Query Cache geschieht durch explizite Angabe und lohnt sich vor allem für Abfragen, die sehr häufig durchgeführt werden:
query.setCacheable(true);
Zusätzlich kann die Query einer so genannten Region zugeordnet werden:
query.setCacheRegion
("<REGION_NAME>");
Durch diese Angabe mit einem frei wählbaren Regionnamen, der einfach als String übergeben wird, können Queries in verschiedene Bereiche unterteilt bzw. zusammengefasst werden.
Über Aufruf der Methode evictQueries()der Hibernate-Klasse SessionFactory kann der Query Cache komplett gelöscht werden. Wird der Methode als Übergabeparameter der Name einer Region übergeben, werden nur die Abfragen aus dem Cache gelöscht, die dieser Region zugeordnet worden sind:
sessionFactory.evictQueries
("<REGION_NAME>");
Das Löschen von im Cache abgelegten Entities geschieht über die Methode evict() bzw. evictEntity(). Der Methode evict() kann eine Klasse übergeben werden. Der Methode evictEntity()- kann der Klassenname der zu löschenden Entity übergeben werden. Es kann beiden Methoden zusätzlich der Wert des Primary Keys einer Entity übergeben werden, um genau einen spezifischen Eintrag zu löschen:
sessionFactory.evict
("<ENTITY_NAME>", "<ID>");
Da die Informationen über die zu löschende Entity über das HTTP-Protokoll von der einen an die andere Anwendung übergeben werden soll, kommt im Cache-Synchronisationsservice die Methode evictEntity() zum Einsatz.
Um Collections aus dem Cache zu entfernen, stellt die SessionFactory die Methode evictCollection() bereit. Auch dieser Methode kann als Übergabeparameter eine ID übergeben werden.
sessionFactory.evictCollection
("<ENTITY_NAME>.<COLLECTION_NAME>",
"<ID>");
Der Name einer Collection setzt sich aus der Entity, in der sie untergebracht ist, sowie dem Namen der Collection selbst zusammen. Soll nun beispielsweise die Zuordnung von Benutzerrollen gelöscht werden, die in der Entity de.ordix.User als Liste von Entities im Attribut roles abgelegt ist, lautet der zu übergebende Parameter de.ordix.User.roles.
Durch die Implementierung eines Moduls zur Synchronisation des Caches ist es möglich, den Second Level Cache in beiden Applikationen trotz beidseitiger schreibender Zugriffe einzusetzen.
Auch wenn die Erstellung des Moduls einen erhöhten, technischen Aufwand bedeutet, der zusätzliche Rechenleistung erfordert und Netzwerkverkehr verursacht, hat sich die Umsetzung unter dem Aspekt der Performance-Steigerung trotzdem gelohnt.
Alexander Zeller (info@ordix.de).