Wenn Sie Hibernate für ORM verwenden und auf eine Funktionalität stoßen, die Multi-Threading erfordert, gibt es viele Fallstricke, die Ihnen das Leben schwer machen können. Dieser Blog wird sich mit diesen Problemen befassen. Die Schlussfolgerung lautet: Verwenden Sie verwaltete Hibernate-Objekte nicht in mehreren Threads.
Betrachten wir eine sehr einfache Domäne: Kunden mit Zahlungen. Jeder Kunde hat eine Reihe von Zahlungen
@Entity public class Client {
...
@OneToMany(cascade=ALL)
private Set Zahlungen = new HashSet();
public void addPayment(double amount) {
payments.add(new Payment(amount));
}
public double getTotal() {
int gesamt = 0;
for (Zahlung Zahlung : Zahlungen) {
gesamt += zahlung.getAmount();
}
Summe zurückgeben;
}
}
@Entity public class Payment {
...
public Payment(double amount) {
this.amount = amount;
}
...
}
Ein sehr einfacher Test besteht darin, einen Kunden mit einigen Zahlungen zu speichern, ihn dann neu zu laden und die Gesamtzahl der Zahlungen erneut zu testen.
public void testSimpleClientWithPayment() throws Exception {
Client client = new Client("ich");
client.addPayment(12.0);
client.addPayment(5.0);
assertEquals(17.0, client.getTotal());
Serializable id = dao.save(client);
dao.flushAndClear();
client = dao.get(id);
assertEquals(17.0, client.getTotal());
}
Natürlich funktioniert das alles wie am Schnürchen und es gibt keine Probleme. Das Projekt geht einfach seinen gewohnten Weg und Sie programmieren weiter.
Themen einführen: Probleme mit Lazy Loading
Irgendwann kommt einer der schlauen Architekten auf die Idee, Berichte über die Gesamtzahlen aller Kunden regelmäßig in separaten Threads zu drucken. Die Berichte werden per Post verschickt oder mit irgendeiner anderen lahmen Ausrede für die Einführung dieses schicken neuen Threading-Dings. Um ihn bei Laune zu halten, entwickeln Sie also einen netten Dienst, der das erledigen kann.
public class ReportingService {
private final class Reporter implements Runnable {
private final Client client;
private Reporter(Client client) {
this.client = client;
}
public void run() {
System.out.println(client + ": " + client.getTotal());
}
}
public void reportTotals() {
for (final Client client : dao.findAllClients()) {
executorService.submit(new Reporter(client));
}
}
...
}
Natürlich testen Sie den Dienst mit einer Reihe von Zahlungen, und alles funktioniert einwandfrei... bis die Funktion in Produktion geht und sich die Protokolle mit diesen Ausnahmen zu füllen beginnen:
21:21:11,562 ERROR [LazyInitializationException, LazyInitializationException.] konnte eine Sammlung von Rolle: domain.Client.payments, keine Sitzung oder Sitzung wurde geschlossen, faul initialisieren org.hibernate.LazyInitializationException: Fehlgeschlagene Lazy-Initialisierung einer Sammlung der Rolle: domain.Client.payments, keine Sitzung oder Sitzung wurde geschlossen at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:358) at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:350) at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:343) at org.hibernate.collection.AbstractPersistentCollection.read(AbstractPersistentCollection.java:86) at org.hibernate.collection.PersistentSet.iterator(PersistentSet.java:163) at domain.Client.getTotal(Client.java:43) at service.ReportingService$Reporter.run(ReportingService.java:18) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:417) at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:269) at java.util.concurrent.FutureTask.run(FutureTask.java:123) at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:650) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:675) at java.lang.Thread.run(Thread.java:595)
Aber das haben Sie doch getestet, oder? Was könnte möglicherweise falsch sein? Nun, die Sammlung der Zahlungen einiger Kunden ist noch nicht initialisiert (=aus der Datenbank geladen), wenn der Kunde dem Executor zur Berichterstattung übergeben wird. Das ist kein Problem: Hibernate kann die Sammlung immer dann nachladen, wenn sie benötigt wird. Zu diesem Zweck ersetzt es die Sammlung durch eine spezielle Implementierung, die einen Verweis auf die Session enthält, die zum Laden der enthaltenen Entität (des Clients) verwendet wurde. Wenn wir payments.iterator() aufrufen, wird die Sammlung über die Session aus der Datenbank geladen. Solange diese Session offen ist, funktioniert der Code. Sessions werden jedoch irgendwann geschlossen. Bei der Transaktionsverwaltung von Federn zum Beispiel sind sie oft mit einer Transaktion verbunden und werden nach Abschluss dieser Transaktion geschlossen. Wenn nun die Transaktion, mit der ein Client mit vielen Zahlungen geladen wird, all diese Aufgaben schießt und dann abgeschlossen wird, bevor die Aufgaben ausgeführt werden, wird die Sitzung geschlossen und die Aufgaben haben keine Möglichkeit mehr, die Zahlungen träge zu laden. Daher die Probleme mit dem Lazy Loading. In einer Testumgebung ist es oft schwierig, dies zu simulieren, da weniger Daten vorhanden sind und Ihre Testinfrastruktur möglicherweise Transaktionen und Sitzungen offen hält, bis alles ausgeführt wurde. In der Produktion werden Sie sich meistens verbrannt haben. Die Lösung ist also einfach, oder? Laden Sie einfach die Zahlungen, bevor Sie die Aufgabe ausführen:
...
for (final Client client : dao.findAll()) {
// --> Lösung:
Hibernate.initialize(client.getPayments());
executorService.submit(new Reporter(client));
}
...
Das scheint einfach genug zu sein. Sie können das Set auch initialisieren, indem Sie eine Methode ausführen, z.B. .size(), oder indem Sie es beim Laden des Clients eifrig abrufen. So, keine Stacktraces mehr im Produktionsprotokoll, Sie haben die Firma gerettet!
Fehlende Updates
Jetzt haben die Business-Analysten eine neue Idee: Der Druck der Summen ist zu einfach, die Kunden wollen jede Woche einen Datensatz in der Datenbank mit der Summe pro Kunde und sie wollen wissen, wann die letzte Summe für jeden Kunden erstellt wurde. Also erweitern Sie Ihren ReportingService um eine neue Methode und eine neue Aufgabe:
...
public class ReportGenerator implements Runnable {
private final Client client;
public ReportGenerator(Client client) {
this.client = client;
}
public void run() {
dao.save(new Report(client, client.getTotal()));
client.setLastCreated(new Date());
}
}
...
Wenn Sie diese Aufgabe nun in Ihrer Testumgebung ausführen, stellen Sie etwas Seltsames fest: Die Berichte werden der Datenbank hinzugefügt, aber das Feld "lastCreated" wird nicht aktualisiert. Woran kann das liegen? Hibernate sollte ein transaktionales Write-Behind mit Dirty Check durchführen: Beim Abschluss der Transaktion sollte geprüft werden, ob sich das Client-Objekt geändert hat, und die geänderten Werte sollten erhalten bleiben. Das funktioniert normalerweise. Warum ist das hier anders? Die Schmutzprüfung wird nur in der Sitzung durchgeführt, in der ein Objekt geladen wurde. Das Client-Objekt wurde in einer anderen Sitzung geladen, in der die Transaktion bereits abgeschlossen wurde. Wenn der Bericht erstellt wird, wird dieser in einer separaten Sitzung gespeichert. Die Infrastruktur der Dao kümmert sich darum, die Sitzung mit dem aktuellen Thread zu verknüpfen und bei Bedarf eine neue Sitzung zu instanziieren.
Sperren ohne Modus
Wie lösen Sie also dieses Problem? Um ein Objekt zu aktualisieren, das aus einer anderen Sitzung geladen wurde, müssen Sie es wieder mit einer neuen offenen Sitzung verbinden. Dazu können Sie die Operation .lock(.., LockMode.NONE) verwenden. Sie müssen dafür sorgen, dass die Sitzung geöffnet bleibt, bis die Aktualisierungen abgeschlossen sind. Um dies zu gewährleisten, verwenden wir eine neue Transaktion für die neue Berichtsaktion:
...
public void run() {
new TransactionTemplate(transactionManager)
.execute(new TransactionCallbackWithoutResult() {
@Override
public void doInTransactionWithoutResult(TransactionStatus status) {
dao.lock(client, NONE);
dao.save(new Report(client, client.getTotal()));
client.setLastCreated(new Date());
}
});
}
...
Die Verwendung dieser Lösung in Kombination mit der vorherigen Lösung führt jedoch zu neuen Ausnahmen:
Verursacht durch: org.hibernate.HibernateException: Unzulässiger Versuch, eine Sammlung mit zwei offenen Sitzungen zu verknüpfen at org.hibernate.collection.AbstractPersistentCollection.setCurrentSession(AbstractPersistentCollection.java:410) at org.hibernate.event.def.OnLockVisitor.processCollection(OnLockVisitor.java:38) at org.hibernate.event.def.AbstractVisitor.processValue(AbstractVisitor.java:101) at org.hibernate.event.def.AbstractVisitor.processValue(AbstractVisitor.java:61) at org.hibernate.event.def.AbstractVisitor.processEntityPropertyValues(AbstractVisitor.java:55) at org.hibernate.event.def.AbstractVisitor.process(AbstractVisitor.java:123) at org.hibernate.event.def.AbstractReassociateEventListener.reassociate(AbstractReassociateEventListener.java:79) at org.hibernate.event.def.DefaultLockEventListener.onLock(DefaultLockEventListener.java:59) at org.hibernate.impl.SessionImpl.fireLock(SessionImpl.java:584) at org.hibernate.impl.SessionImpl.lock(SessionImpl.java:576) ...
Das liegt daran, dass wir versuchen, die Sammlung, die wir zuvor so geschickt initialisiert haben, auch mit der neuen Sitzung zu verbinden. Diese Sammlung wird jedoch versuchen, einen Verweis auf eine offene Sitzung zu behalten, aber nicht auf zwei. Das Gute daran ist, dass wir die Sitzung nicht mehr initialisieren müssen, wenn wir den Client mit einer neuen Sitzung verbinden: Das faule Laden funktioniert wieder. In manchen Situationen ist es jedoch sehr schwierig, festzustellen, welche Objekte bereits initialisiert sind und welche nicht und wann welche Objekte mit der neuen Sitzung verbunden werden müssen usw... Die Verwendung der Lock-Operation ist oft eine mühsame Angelegenheit!
Geben Sie einfach den Ausweis ab... Dummkopf!
Nachdem Sie sich eine Weile mit dem Problem herumgeschlagen haben, geben Sie auf und entscheiden sich für eine viel einfachere Lösung: Sie speichern das Objekt in der Datenbank und geben den ID-Wert an den Worker-Thread weiter. Dieser Thread kann nun die Daten aus der Datenbank neu laden und sicher daran arbeiten.
Fazit
Die Übergabe von in Hibernate verwalteten Objekten (wie Sammlungen oder Proxies) an andere Threads kann zu schwer nachvollziehbaren Problemen führen, wie z.B:
- Probleme mit träger Initialisierung
- Fehlende Updates
- Ausnahmen sperren
Daher ist es oft besser, die von Hibernate verwalteten Objekte nicht an andere Threads zu übergeben, sondern die Objekte zu speichern und sie nach ID aus der Datenbank im anderen Thread wieder zu laden.
Verfasst von
Maarten Winkels
Contact
