Obwohl Toplink [1] als Unterprodukt des Oracle Application Server angeboten wird, pictureet es das gesamte Spektrum des objektrelationalen Mappings einfacher Java-Objekte bis hin zum Einsatz von Enterprise Entity Beans ab. Wobei wir uns primär auf Ersteres konzentrieren wollen. Man könnte sicher ein ganzes Buch über das Thema Datenbankperformance schreiben, dieser Artikel versucht trotzdem wesentliche Aspekte in der Anwendung von Toplink und bei der Suche nach Geschwindigkeitsreservoirs aufzuzeigen.
Drei Ratschläge für Anfänger ...
- Keep it simple! Toplink bietet ein enorm breites Funktionsspektrum. Komplexe Caching-Strategien, Objekt-Factory- und Clone-Konfigurationen, händische SQL Statements ... die Liste der Features ist lang. Gut beraten sind Einsteiger mit einem einfachen Objektmodell (JavaBeans-konform), das sich möglichst eins zu eins in Tabellen überführen lässt.
- Lass das jemand machen, der das schon mal gemacht hat. Umfangreiche Mappings, komplexe Datentransformationen und Zielstrukturen erschweren die Arbeit mit der Oracle Mapping Workbench, dem Tool zum Festlegen der Abbildung zwischen Objekten und Tabellen. Hier empfiehlt sich, bei Großprojekten eine Person exklusiv abzustellen, die sich ausschließlich um Toplink-Aufgaben kümmert, um das Risiko von Fehlkonfigurationen nicht unnötig über viele Köpfe zu verteilen.
- Das ist nur ein Stück Software, keine Wundermaschine. Eine häufige Aussage, die ich schon hörte: Toplink muss doch wissen, was ich meine. Leider ist es aber nur ein Softwareprodukt und verlangt umfangreiches Know-how von seinen Benutzern. Hier sind Architekten und Designer gefragt, die Toplink von anderen Anwendungsschichten so kapseln, dass die Framework-spezifischen Klassen weder Objektmodell- noch Controller-Schichten verwässern und die meisten Entwickler daher nicht tangieren.
Schmerzen mit dem Objektmodell
Grundsätzlich fängt man mit dem Mapping sinnvollerweise erst an, wenn das Objektmodell sich im Design stabilisiert hat. Alles andere treibt Toplink-Mapper- und DB-Administratoren in die Verzweiflung und lässt sich auch mit Objektserialisierung vorübergehend umschiffen. Das Modell selbst sollte einfach gehalten werden (JavaBeans) und eine in sich geschlossene Gesamtentität mit einem Root-Element repräsentieren. Enthält ein solches Modell mehrere Root-Elemente, sprich Objekte, die Teilgruppen des Gesamtmodells repräsentieren, parallel in der Datenbank einem Update unterzogen werden oder aufeinander verweisen, lässt das Datenchaos oft nicht lange auf sich warten (der Auftrag kennt den Kunden und der Kunde kennt alle seine Aufträge, klassische m:n-Beziehungen). Die Medizin lautet oft Optimistic Locking, das Aufspüren paralleler Datenänderungen auf Anwendungsebene. Oft ist das Handling der dann auftretenden Exceptions aber mit der Fehlermeldung für den Benutzer verbunden: Tut mir leid, aber die Änderungen der letzten halben Stunde sind weg, es gibt noch andere User außer dir - oft vermeidbare Risiken aufgrund von ungünstigem Design.Ähnlich riskant ist die Vermischung zwischen Businesslogik und Objektmodell. Getter- und Setter-Methoden mit kleinen Constraints geschmückt, die bei Übergabe von Null-Values Toplink auf die Bretter schicken, überschriebene clone()-Methoden, die Toplink selbst zum Kopieren von Objekten verwendet, selbst implementierte Collection-Klassen, eigene und fremde Lazy-Fetch-Mechanismen, Nachladen und Erzeugen von Objekten über Factories statt über new... die Liste der Infektionsherde für scheinbare Toplink-Krankheiten ist groß.
In allen Fällen gilt: Toplink muss das gleiche Objektmodell verwenden wie der Anwender selbst, leider besitzt es nicht die gleiche Intelligenz und Toplink selbst hat weniger Fehler als man glaubt, es leidet oft mehr unter Fehlern anderer. Einige Nutzer garnieren das Gericht noch mit einem selbst gebauten Exception Handler, einer Klasse, die falsch angewendet, jede Fehlermeldung von Toplink im Nirwana verschwinden lassen kann.
Selbstgeschnittener Datensalat
Auch gerne gefunden im Gruselkabinett der Absonderlichkeiten: direkte, modifizierende SQL-Zugriffe auf die Datenbank. Dabei spielt es keine Rolle, ob Sie Ihr Update über Toplink machen oder nicht. In beiden Fällen erzeugen Sie Datensalat, der mit dem Zustand, den Toplink in seinem Objekt-Cache kennt, nicht übereinstimmt. Gleiche Wirkung haben Stored Procedures und Datenbank-Constraints (on delete cascade), die an den Daten manipulieren, ohne dass Toplink dies mitbekommen kann. Einzige Rettung: Refreshen des Cache-Zustandes vor dem nächsten Commit. Oft artet dies aber aus. Entweder wird jedes Objekt jetzt immer refresht, was einem Deaktivieren des Cache gleichkommt, oder man löscht Toplink den Cache unterm Hintern weg (removeFromIdentityMap oder initializeIdentityMap). In allen Fällen sitzt man schnell in der Problemschlinge. Deshalb: Updates - solange Toplink aktiv ist - über Toplink-Objekte (auch wenn's manchmal wehtut), die Datenkonsistenz wird es Ihnen danken.Wann sind zwei Objekte eigentlich gleich?
Eine der ureigensten Fragen der Java-Sprache, die Objektidentität, wird bei Verwendung von Entitäten aus Datenbanken plötzlich ganz anders beantwortet. Da sind zwei Objekte nicht gleich, wenn ihre equals()-Methode true oder ihr Hashcode den gleichen Wert aufweist, sondern wenn ihre Primärschlüssel übereinstimmen. Eine Identität ist dann plötzlich eine Zeile in einer Datenbanktable. Somit kann es dann leicht beliebig viele Objekte mit dem gleichen Primärschlüssel in derselben Anwendung geben und schnell widerspricht das eine dem anderen.Toplink hat bei dieser Identitätsfrage ein ähnliches Problem wie Sie, denn es muss entscheiden, was es eigentlich beim Commit in der Anwendung tun soll. So behält es standardmäßig immer eine eigene Kopie des Originals in seinem Cache und gleicht diese beim Commit mit der geklonten Arbeitskopie von Ihnen ab. Aus dem Delta ermittelt es die durchzuführenden Aktionen (Insert, Update ...). Am besten fährt Toplink, wenn man ihm einen sinnvoll großen Cache (pro Teilelement der Gesamtentität!) gibt, die Existenzprüfung auf assumeExistenceForDoesExist setzt (ein existierender Primärschlüssel deutet dann für Toplink auf ein existierendes Datenbankobjekt hin) und Toplink die Erzeugung des Schlüssels für das Objekt selbst überlässt. Wer Primärschlüssel selbst erzeugt oder zur Laufzeit manipuliert, dem blüht schon bald Ungemach.
Transaktionsmanagement? Nein, danke ...
Ich persönlich bevorzuge, Toplink im Three-Tier-Modus zu verwenden, unabhängig davon, ob es sich um eine Fat-Client-Anwendung handelt oder ein echtes, vielleicht sogar verteiltes Multi-Tier-System. Besonders dort, wo viele konkurrierende Threads parallel auf Toplink zugreifen, sind isolierte Objektbereiche, konsistente Cache-Synchronisation und Connection Pools gang und gäbe. Dabei wird jedem Thread eine eigene ClientSession als Arbeitsbereich zur Verfügung gestellt, beim Transaktions-Commit werden diese separaten ClientSessions mit der ServerSession, dem applikationsweiten Objekt-Cache, abgeglichen. Bliebe nur die Frage: Wo fängt die Transaktion (UnitOfWork genannt) an und wo hört sie auf?Besonders Webanwendungen tun sich aufgrund der Zustandslosigkeit des HTTP-Protokolls und trotz Session-Management im Servlet Layer schwer mit Langzeittransaktionen, die sich über mehrere JSPs hinweg erstrecken. Oftmals Lösung für dieses Problem ist die Zwischenspeicherung von Toplink Session/UnitOfWork in der Http-Session entweder im Original oder in einer ähnlichen Repräsentation. Dabei zieht dann diese proprietäre Toplink-Transaktion ihre Kreise durch alle Schichten. Bei diesem Modell A, wie ich es hier bezeichnen möchte (Listing 1 und Abb. 1), wird zunächst irgendwann beim Starten der Applikation eine ServerSession erzeugt, aus der heraus dann Thread-bezogene ClientSessions erzeugt werden. Über die ClientSession wird mittels einer so genannten Expression (einer Art SQL-Where-Klausel) ein Objekt (oder mehrere) gelesen (1). Diese so aus der Datenbank geholten Objekte müssen nun bei einer UnitOfWork (einer Transaktion) registriert werden (2). Hierbei werden künstlich Kopien der Objekte erzeugt - was natürlich auch Performance kostet -, die dann in der Anwendung modifiziert werden dürfen (3). Da Toplink Referenzen auf die Originalobjekte und die erzeugten Kopien hat, veranlasst ein Commit auf der Transaktion Toplink zum Abgleich zwischen Original und Klon-Kopie (4,5). Ergebnis dieses Abgleichs sind SQL Statements, die dann gegenüber der Datenbank den Objektzustand in Form von Tabelleneinträgen abpictureen sollen (6).
Listing 1
Modell A - Transaktionsmanagement mit langlebiger UnitOfWork// Login beim Start der AnwendungProject project = ...Server serverSession = project.createServerSession(5,10);[...]// Auslesen und Registrieren des ObjektesClientSession cs = serverSession.acquireClientSession();Expression exp = (new ExpressionBuilder()).get("id").equal(1234);Kunde einKunde = (Kunde) cs.readObject(Kunde.class, exp);UnitOfWork uow = cs.acquireUnitOfWork();Kunde einKundeClone = uow.registerObject(einKunde);[...]// ModifikationeinKundeClone.setName("Müller");[...]// Festschreiben der Modifikationenuow.commit();uow.release();cs.release();
Problem der Variante A bleiben UnitOfWork und ClientSession, die über die komplette Transaktionszeit hinweg überleben müssen, um schließlich commitet werden zu können. Um diese Schwierigkeit zu umschiffen und das Anlegen unnötiger Transaktionen zu verhindern, die z.B. nie commitet werden, weil der Benutzer vielleicht nie den Submit-Button klickt, kann man (abhängig von der Aufgabenstellung) auch komplett auf das Überleben der Transaktion verzichten. Beim Merging-Modell B (Listing 2) wird die ClientSession nach dem Lesen des Objektes mit release() wieder vernichtet. Das Klonen des Originals für Arbeitskopien können Sie entweder per UnitOfWork.register() durchführen oder selbst eine händische Kopie anlegen. Haken bei dieser Lösung bleibt die etwas geringere Performance, dadurch dass das Objekt für die ClientSession beim Commit noch einmal komplett abgeprüft werden muss, auch ist das gemeinsame Committen mehrerer Änderungen an unterschiedlichen Objekten in einer Transaktion nicht selten schwieriges Handling.
Listing 2
// Login beim Start der AnwendungProject project = ...Server serverSession = project.createServerSession(5,10);[...]// Auslesen und Klonen des ObjektesClientSession cs = serverSession.acquireClientSession();Expression exp = (new ExpressionBuilder()).get("id").equal(1234);Kunde einKunde = (Kunde) cs.readObject(Kunde.class, exp);UnitOfWork uow = cs.acquireUnitOfWork();Kunde einKundeClone = uow.registerObject(einKunde);// Löschen der Transaktionsbeziehunguow.release();cs.release();[...] // Modifikation des Klon// pro ThreadClientSession cs2 = serverSession.acquireClientSession();// Erzeugung einer neuen Transaktion zum Commit des Klon-ZustandesUnitOfWork uow2 = cs2.acquireUnitOfWork();uow2.deepMergeClone(einKundeClone);uow2.commit();uow2.release();cs2.release();
Toplink in J2EE
Eine dritte Variante ist die Einbindung der Toplink-Transaktion in die globale Transaktion eines Application Server Container. Bei dieser JTA/JTS-(Java Transaction Service-)Lösung [2] hängt sich Toplink als Synchronisation Listener in den globalen Transaktionskontext und kann dann ein gemeinsames Commit mit anderen (auch nicht DB-spezifischen) Quellen durchführen. Toplink lässt sich überdies sehr nah in den Application Server integrieren, u.a. auch in der Nutzung von gemeinsamen Connection Pools über JNDI, ganz abgesehen von der Unterstützung für Entity-Bean-Persistenz.Caching, kein Problem?
Zurück zum Standard-Toplink-Einsatz. Caching ist ein probates Mittel, um Performance deutlich zu steigern, auch wenn Toplink erlaubt, das Caching auch komplett zu deaktivieren. Leider erkennt Toplink nicht, wenn Sie ein Objekt in unterschiedlichen Caches haben, weil Sie beispielsweise zunächst einen Subteil (z.B. Adresse) und dann noch mal das Komplettobjekt (z.B. Kunde) gelesen haben. Toplink kann auch bereits getätigte SQL Selects nicht als Quasi-Prefetching von Daten behandeln (select * from adresse) und wird beim Lesen des Gesamtobjektes schön all das noch einmal lesen, was es eben schon mal serviert bekommen hat.Caching in Toplink ist trotzdem kein Kinderspiel. Ist z.B. der Soft-Cache für Sie der richtige, wenn Sie einen RMI-Server haben? Ein Einstieg in die Tiefen der Cache-Typen-Dokumentation ist hier definitiv von Vorteil. Ist der Cache zu groß, wird der Performancevorteil durch ständige Suchaktionen ad absurdum geführt. Ist er zu klein, kann die Objektidentitätsprüfung zu zweifelhaften Inserts führen, wo Updates notwendig wären, weil das Objekt Toplink plötzlich unbekannt vorkommt. Und ist er überhaupt sinnvoll? In einem J2EE Cluster muss der Zustandswechsel eines Objektes im Cache ggf. über n Toplink-Sessions im Netz synchronisiert werden. Dieser Wechsel der Prozessgrenzen kostet nicht nur sehr viel Performance, sondern kann auch zu aufwendiger Implementierung in der Persistenzschicht führen.
Voreilige Performancehörigkeit?
Nicht selten entwickeln sich leidvolle Erfahrungen beim Umgang mit objektrelationalen Produkten. Irgendwie ist die Performance ab einer bestimmten Objektkomplexität sehr schnell miserabel, was letztendlich an der Größe und dem Abgleich- und Transformationsaufwand zwischen Objekt und Tabelle liegt. Die angestrebte Lösung ist nicht selten eine Mischform, in der man sich erlaubt, nativ mit SQL, dem Holzhammer der Leidgeplagten, die Geschwindigkeitsnachteile wieder aufzuholen. Nur wozu verwendet man dann überhaupt Toplink? Unangenehm zu Buche schlagen diese zuweilen schmutzigen Tricks, wenn es von einer Single-VM-Umgebung zu einem geclusterten Multitier-System geht. Jede eingeschleppte Factory- oder Lazy-Fetching-Abhängigkeit kann hier bis zu einem kompletten Redesign der Anwendung führen.Meine Meinung ist: Heben Sie sich das Problem der Performance ein wenig auf und ziehen Sie erst mal die restliche Architektur hoch. Toplink unterscheidet nicht zu unrecht zwischen reinem O/R Mapping und SQL-Datenbankabfragen in Form von Reports. Niemand erzeugt 2.000 Objekte, um von allen den maximalen Wert eines Feldes aufzuspüren. Eine strikte Trennung zwischen O/R- und Data-Read-Aktivitäten ist dringend geboten. Mit der berühmten Suchanfrage, mit der Sie einen Kunden finden, suchen Sie zunächst über ein Report SQL dessen Objekt-ID aus der Datenbank und lesen erst dann zur ID über Toplink das Kundenobjekt, oder nicht?
Performance: ein Problem, vier Lösungen?
Die Toplink-Autoren spendieren dem Thema Performance ein eigenes Subkapitel ihrer Dokumentation, warum wohl? Die unterschiedlichen Lösungen, die dort vorgestellt werden, beruhen stets auf wenigen einfachen Prinzipien:- Reduktion der Datenmenge: z.B. durch explizite SQL Report Queries mit wenigen Attributen.
- Verringerung der Anzahl der Zugriffe: z.B. durch Joinen von Tabellen beim Auslesen von Daten, Cursor-Verarbeitung mit Prefetching von größeren Datenmengen, Anwendung von Caching-Strategien.
- Umorientierung von Dynamic zu Static SQL: Durch Named Queries, die der Datenbank zuvor bekannt gemacht werden, können feste Zugriffspfade auf die Daten errechnet werden. Im Gegensatz zu reinem dynamischen SQL, bei dem die Datenbank das SQL immer wieder neu auswerten und Zugriffswege errechnen muss, werden hier feste Zugriffs-SQLs vordefiniert.
- Erhöhung der Anzahl der parallelen Zugriffe durch Connection Pooling. Beim Einsatz von vielen parallelen Verbindungen können Threads nebenläufig Aktionen auf der Datenbank durchführen.
Messbarkeit, ein Problem für sich
Bei hoch integrierten Systemen und viel fremdem Bytecode scheint es zunächst schwer, dem Gefühl, es läuft zu langsam, konkrete Messzahlen entgegenzusetzen, geschweige denn Lasttests durchzuführen. Dies liegt primär an der Zahl der unterschiedlichen Schichten (in Abb. 2 beispielhaft mit den Buchstaben A-H bezeichnet).
- A/B: In den Schichten A und B können Sie Performancemessungen leicht selbst implementieren. Abfrage der Systemzeit, Deltaberechnung, Logging in Dateien oder auf den Bildschirm. Bedenken Sie aber, dass Ihre Messpunkte nicht selbst zum Performancehindernis werden, z.B. wenn Sie jeden einzelnen Aufruf mit allen Parametern mitschreiben.
- C: In der Toplink-Schicht empfiehlt sich auf jeden Fall die Aktivierung des Performance Profiler auf der Session. Obwohl zuweilen stiefmütterlich verwendet, ist der Profiler ein gutes Werkzeug, um auswerten zu können, wo auf der Strecke zwischen Anwendung und Datenbank die meiste Zeit vergeht (beim Zugriff, beim SQL, beim Objektaufbau ...?).
- D: Wenn Sie zwischen Toplink und dem JDBC-Treiber einen externen Connection Pool über JNDI schalten, stehen Ihnen hier die Logging-Möglichkeiten des jeweiligen Application Server bei, um Geschwindigkeit und Durchsatzraten zu ermitteln.
- E: Einige JDBC-Treiber verfügen über explizite Debug- und Logging-Möglichkeiten. Wenn Ihr Treiber dies nicht unterstützt, können Sie sich mit etwas Aufwand auch einen eigenen JDBC Wrapper schreiben. Dies ist nicht allzu schwierig, Sie leiten alle Aufrufe durch ihren Quasi-Proxy an den Treiber weiter und stellen somit einen eigenen JDBC-Treiber mit Logging her.
- F: Obwohl die meisten Entwickler abwinken, wenn es um Nicht-Java-Komponenten-Tools geht, Performancemessungen und Tracing im Netz sind durchaus auch ein sinnvolles Mittel bei der Problemsuche. Kleine Tools wie z.B. TCPTrace [3] können Portadressen umleiten und damit die Kommunikation zwischen Client und Datenbank-Server mitlesen.
- G: Nicht selten liegen große Performancereservoirs auch in der Datenbank. Auch diese bietet normalerweise in Enterprise-Produkten wie DB2 und Oracle umfangreiche Tracing-Möglichkeiten.
- H: Wenn Sie an Performancetests im laufenden Betrieb interessiert sind, weil nur in einer produktiven Umgebung manche Tests sinnvoll durchführbar erscheinen, ist JMX [4] als Einstiegspunkt in die laufende Anwendung eine sinnvolle Erweiterung. Implementieren Sie in JMX explizite Testläufe, die Sie in gleichen Abständen anstarten, um die Geschwindigkeit der Gesamtanwendung über die Zeit mit zu verfolgen.
Oft genanntes Hindernis bei Performancetests: aufwendige Kodierung. Viele Messpunkte über die Anwendung verteilt, von Toplink gekapselte Datenbankaufrufe und die Notwendigkeit produktiven Source nicht mit Testcode zu überschwemmen. Eine enorme Hilfestellung kann hier die Verwendung von aspektorientierter Programmierung z.B. mit AspectJ [5] bieten. Kodieren Sie Aspekte, die die SQL Connection und Statementaufrufe messbar machen. Mithilfe von AspectJs Bytecode-Modifikation durchdringen dieses Messpunkte auch fremden Bytecode und sind nahezu ohne Aufwand jederzeit aus dem Source wieder entfernbar. Auch der Einsatz von Java-Profilern wie JProbe [6] kann bei der Performancesuche helfen, der Aufwand für das Aufsetzen solche Tests ist allerdings auch nicht zu unterschätzen.
Datenbank-Latein
Interessanterweise wird die Performance von Java-Applikationen immer noch häufiger angezweifelt als die von Datenbanken oder DBM-Systemen. Dabei beginnt für viele Performancehungrige hier das unentdeckte Land. Wissen Sie, wie viele Connections von der Datenbank Ihnen zur Verfügung stehen? Welche SQLs werden am häufigsten ausgeführt? Welche Indizes auf Where- oder OrderBy-Elemente fehlen? Wie exakt sind Ihre Where-Bedingungen formuliert? Welche Teile von dynamischem JDBC lassen sich durch statistische Datenbank-Views optimieren? Welche Zugriffspfade nimmt der Datenbank-Optimizer bei der Erzeugung Ihrer Ergebnismenge? Wie viele Full-Table Scans laufen auf Ihrer Anwendung? Wann wurden die Statistiktablen der Datenbank das letzte Mal aktualisiert? Wann lief der letzte Reorg der Datenbank? Wie sieht der Isolation Level für parallele Datenbankzugriffe auf Ihrer Datenbank aus? Wie viel Speicher hat ihr DBMS, auf welchem Dateisystem geschieht die Datenorganisation? Wie groß sind Prefetching und Logfile-Größen? ...Sie sehen, das Thema Performance lässt Freiraum für enormes Potenzial in allen Bereichen. Vermindern Sie beim Design neuer Anwendungen das Risiko von Fehlern durch Verringerung der Komplexität und machen Sie jede einzelne Architekturkomponente für sich messbar, um an den wirklich großen Performanceschrauben zu drehen. Caching ist nur einer von vielen Lösungsansätzen dabei.
Lars Wunderlich arbeitet in seiner Funktion als Software Engineer als Systemarchitekt und OO-Coach im Bereich der Entwicklung von Unternehmensanwendungen mit J2EE-Technologie bei TUI InfoTec.
Links und Literatur
[1] otn.oracle.com/products/ias/toplink/
[2] java.sun.com/products/jta/
[3] www.pocketsoap.com/tcptrace/
[4] java.sun.com/products/JavaManagement/
[5] eclipse.org/aspectj/
[6] www.quest.com/jprobe/




