Performance-Optimierungen mit JPA

Ihre Anwendung könnte schneller antworten, der Fachbereich ist nicht wirklich zufrieden – aber Entwickler und Datenbank-Administratoren finden keine Optimierungen mehr? Wie kann man die Performance ohne eine Investition in potentere Hardware – wenn dies überhaupt einen Effekt hat – verbessern und welche Fallstricke werden oft übersehen?

Die Situation

Die Anwender beschweren sich nur selten und mit den Testdaten funktioniert es bestens. Eine der Ursachen, aber auch die Vorzüge des Java-Persistence-API (JPA) liegen darin, dass es eine sehr gute Abstraktion von relationalen Datenbanken darstellt. Spätestens bei der Betrachtung der Performance müssen der relationale und der objektorientierte Ansatz jedoch unbedingt gemeinsam betrachtet werden. Dieser Artikel zeigt durch bewährte Lösungen für häufig auftretende Probleme Wege zu besserer Performance auf.

JPA wurde als JSR 220 schon im Jahr 2006 veröffentlicht und feierte damit bereits sein mehr als 10-jähriges Jubiläum. Schon deutlich früher konnte man Berührung mit heute gängigen Implementierungen wie Hibernate haben, heute ist die Technologie aus Projekten mit SQL-Datenbanken nicht mehr wegzudenken. JPA geht dabei als Framework für objektrelationales Mapping von zwei wesentlichen Annahmen aus: Die Denkweise des Entwicklers liegt in der objektorientierten Welt. Objekte, Attribute und Relationen bestimmen das Denken, nicht SQL-Befehle, Cursor und Ausführungspläne. Außerdem befindet sich die Anwendung in der Regel auf einem physisch getrennten System, dem Anwendungsserver.

Die Wurzel des Übels

Genau diese Trennung von Datenbank und Anwendungsserver ist die Ursache für den Großteil der Probleme. Im Gegensatz beispielsweise zu Stored Procedures kostet selbst ein einfacher Zugriff auf die Datenbank messbare Zeit, dadurch wird die Anzahl von Interaktionen mit der Datenbank plötzlich relevant. Außerdem ist häufig die Anzahl der Zugriffe aus Sicht des Entwicklers nicht deterministisch, weil sie zur Laufzeit von den konkreten Daten abhängig ist. Es spielt eine Rolle, ob für zehn Postleitzahlen nur eine oder sogar zehn Städte geladen werden müssen. Beides zusammen führt zu Performance-Problemen – schlimmstenfalls nur manchmal.

Abhängig vom konkreten Daten-Szenario funktioniert ein Anwendungsfall, ist einfach nur langsam oder läuft auf Timeouts und ist schlimmstenfalls überhaupt nicht nutzbar. Wesentliches Merkmal dabei ist die Latenz im Netzwerk („Ping Roundtrip“) beziehungsweise die Laufzeit einer einfachen SQL-Abfrage mit leerer Ergebnismenge für die Java-Anwendung.


Selbst wenn die Latenz im lokalen Netzwerk auf den ersten Blick mit unter einer Millisekunde gut aussieht, ist diese unter Last nicht garantiert und verschlechtert die Zeit für eine Interaktion mit der Datenbank.

Probleme analysieren

Trifft man auf ein solches Problem, gilt es zuerst, es nachstellbar zu machen und auf den betroffenen Systemen zu analysieren. Sollte sich hier eine Ursache nachweisen lassen, sind die jeweiligen Ansprechpartner mit einer Analyse und Problemlösung zu betrauen. Sie werden etwa einen fehlenden Index in der Datenbank anlegen oder den Java-Code optimieren. In diesem Beispiel gehen wir jedoch von folgendem Szenario aus: Die Performance ist nicht ausreichend, obwohl sowohl Datenbank- als auch Applikationsserver keine nennenswerte Belastung zeigen. Daher sind folgende zusätzliche Analysen interessant:

  • Transaktionen und SQLs: Die Anzahl der Transaktionen insgesamt sowie die jeweils in einer Transaktion ausgeführte Anzahl der Statements
  • Top-10-SQL-Statements: Getrennt nach Anzahl der Ausführungen sowie maximaler und kumulativer Laufzeit
  • Thread-Analyse der Java VM: Mit welchen Methoden wird wie viel Zeit verbracht? Im einfachsten Fall kann geprüft werden, wo die Ausführung bei in kurzen Abständen wiederholten Thread-Dumps steht, etwa mit den Werkzeugen „jstack“ oder „jvisualvm“
  • Statistiken des „PersistenceContext“: Etwa mithilfe der Hibernate-Statistiken einen Blick auf die Anzahl und Laufzeiten aus Sicht des Anwendungsservers ermöglichen

Im Szenario ergeben die genannten Analysen folgendes Ergebnis: Die Datenbank-Analyse und die Hibernate-Statistiken zeigen viele Statements pro Transaktion, hierbei sind die Statements allerdings schnell und außerordentlich häufig. Die Java VM verbringt die Zeit normalerweise in einem Methoden-Aufruf, der versucht, Daten aus der JDBC-Verbindung zu lesen. Beide Ergebnisse zusammen beweisen, dass die Zeit durch die Kommunikation auf dem Netzwerk-Medium verbraucht wird, obwohl dieses ebenfalls nicht ausgelastet ist. Lokale Kommunikation beschleunigt den Code extrem, hohe Bandbreiten im Netzwerk lösen jedoch leider nicht das Problem der Latenz und grundsätzlicher Grenzen von TCP.

Daten schreiben

Aus einer Klasse ist schnell eine JPA-Entität erstellt: Es genügt, ein POJO mit wenigen Attributen um Annotationen zu erweitern, und schon können Daten in die Datenbank geschrieben werden. Im folgenden Beispiel sollen Geodaten des Projekts OpenStreetMap aus einer CSV-Datei in eine Hierarchie von drei Objekten (Bundesland, Stadt und Postleitzahl) mit den dargestellten Häufigkeiten geschrieben werden:

Insgesamt handelt es sich dabei um ca. 24.000 Objekte, die den Weg in die Datenbank antreten. Am Beispiel der Entität „City“ sieht das in einer ersten Version wie in folgendem Listing aus.

@Entity
public class City {
 @Id @GeneratedValue long id;
 String name;

 @ManyToOne State state;
 @OneToMany(mappedBy="city")
 Collection<PostalCode> postalCodes;

//  … getter und setter …
}

Die Daten liegen im CSV-Format als kartesisches Produkt vor, zum Beispiel vervielfachen sich die 16 Bundesländer auf alle Zeilen der im CSV enthaltenen 13.381 Postleitzahlen. Jedes fachliche Objekt wird beim ersten Vorkommen in einer Zeile des CSV erzeugt, persistiert und für folgende Zeilen weiter als verknüpftes Objekt genutzt, hier am Beispiel einer Stadt mit dem zugehörigen Bundesland (siehe Listing 2).

State state = ...
...
City city = new City();
city.setName(name);
city.setState(state);
entityManager.persist(city);

Diese erste Version lief mit einer lokalen Datenbank (Java DB) ca. 17 Sekunden inklusive Einlesen des CSV. Die oben angesprochene Thread-Dump-Analyse zeigte in 7 von 10 Fällen als aktive Zeile die Standard-Strategie zur automatischen Erzeugung einer ID für das Objekt als Verursacher. Die Statistiken von Hibernate zeigten fast 72.000 SQL-Statements – mit einer über LAN direkt erreichbaren MySQL-Datenbank lag die Laufzeit in diesem Szenario bei deutlich mehr als vier Minuten. Im LAN ist der Zugriff auf ein physisches Netzwerk-Medium im Spiel, das im Fall der lokal laufenden Datenbank völlig ausgeblendet wurde.

Die Optimierung des ID-Generators durch Erzeugen mehrerer IDs in einem Datenbank-Zugriff sieht im Quelltext wie folgt aus und reduziert sowohl die Statement-Anzahl als auch die Laufzeit auf etwa ein Drittel.

@Id
@TableGenerator(name = "city", initialValue = 1000, allocationSize = 1000, table = "sequences",
 pkColumnName = "name", valueColumnName = "value")
@GeneratedValue(generator="city")
long id;

Hier wurde der Tabellen-Multi-ID-Generator gewählt, da dieser auf allen Datenbanken funktioniert. Der Sequenz-Generator hat prinzipiell das gleiche Problem vieler Datenbank-Zugriffe, kommt aber insgesamt mit deutlich weniger SQL-Statements aus, da keine getrennte Transaktion erforderlich ist. Noch besser können beispielsweise UUID-Generatoren sein, die gänzlich ohne Datenbank-Zugriff auskommen. Hier ist für die Datenbank zu prüfen, wie diese mit VARCHAR-Spalten als Primärschlüssel umgeht und ob numerische Schlüssel nicht besser indizierbar sind.

Weitere Optimierung: Batch-Sortierung

Führt man nach dieser ersten Optimierung den Code erneut aus und analysiert das Laufzeitverhalten erneut mittels Thread-Dumps, so sieht man einen weiteren Hauptverursacher. Das abwechselnde Schreiben von Städten und Postleitzahlen, die nahezu eine „1:1“-Beziehung aufweisen, nimmt JPA die Möglichkeit, die Zugriffe per JDBC-Batch zu optimieren. Im Thread-Dump sieht man das sowohl an der „NonBatchingBatch“-Implementierung von Hibernate als auch auf der Datenbank anhand der Anzahl von Ausführungen der Insert-Statements. Werden die Objekte gemäß ihrer Hierarchie persistiert – also zuerst alle Bundesländer, dann alle Städte und zum Schluss die Postleitzahlen – kann mithilfe des Parameters „hibernate.jdbc.batch_size“ die Anzahl der Datensätze je Interaktion mit der Datenbank erhöht werden. Durch diese zwei geringfügigen Änderungen lässt sich die Performance des Anwendungsfalls insgesamt um den Faktor 10 auf unter zwei Sekunden verbessern.

Die Laufzeit verändert sich mit größer werdender Batch-Size sehr gut, die Performance-Gewinne werden jedoch auch schnell wieder geringer. Der Effekt der einzelnen Veränderung der „AllocationSize“ wie auch der „BatchSize“ ist für Werte oberhalb von 20 noch messbar, aber nicht mehr signifikant. Ab dieser Größe beginnen andere Faktoren im Netzwerk zu wirken, die diese Optimierungen begrenzen.

Lesen von Daten in der objektrelationalen Welt

Die geschriebenen Daten werden zu einer späteren Zeit wieder aus der Datenbank gelesen. Hier tritt häufig das sogenannte „1+n-Problem“ auf: Wird eine Instanz wie eine Stadt gelesen, so wird von JPA die Relation zum zugehörigen Bundesland („State“) hergestellt. Allgemein formuliert, werden für alle Objekte, die die eigentliche und erste („1“) Abfrage gefunden hat, alle über „…ToOne“-Relationen verbundenen Objekte nachgeladen. Dazu werden viele einzelne Abfragen auf deren Primärschlüssel ausgeführt – insgesamt „n“-mal, daher „1+n“.

Die konkrete Anzahl der Abfragen ist abhängig von der Struktur des Datenmodells, aber auch vom Inhalt der Daten – und damit unberechenbar. Mögliche Lösungswege aus dem „1+n-Problem“ sind:

  • „Lazy“-markieren der „ManyToOne“-Relation, wenn die Daten meist nicht benötigt werden
  • Markieren als „eager“, wenn die Daten immer relevant sind
  • „Lazy“-markieren, aber in der Abfrage je Anwendungsfall angeben, welche Daten per JOIN geladen werden sollen, dabei sind „NamedQueries“ von ihren Möglichkeiten her identisch zum „Criteria“-API
  • Caching der Daten – bevorzugt bei unveränderlichen Daten

„Lazy“-Loading ist für den Persistenz-Provider nur ein Hinweis, „Eager“-Loading kann in Abfragen nicht immer berücksichtigt werden. Daher ist mittels Log-Analyse zu prüfen, wie die ausgeführten Statements aufgebaut sind. Einige JPA-Implementierungen benötigen dafür auch instrumentierten Bytecode, der die Zugriffe auf die Objekte und Attribute verändert sowie Proxy-Objekte möglich macht. Für das „Eager“-Loading führen viele „FETCH JOINs“ durch das resultierende kartesische Produkt zu langen und auch breiten „ResultSets“ auf der Datenbank. Dies ist sowohl für die Datenbank als auch die Java VM eine unnötig große zu übertragende Datenmenge.

Sollten die Standard-Einstellungen nicht ausreichen, kann eine weitere kontrollierte Abfrage helfen, die Daten zu laden, das sogenannte

Prefetch Query

Für einen Warenversand sollen eine Menge Postleitzahlen mit allen bezogenen Daten geladen werden, dazu dient im ersten Versuch die Abfrage

SELECT e FROM PostalCode e WHERE e.code IN (:codes)

für den Zugriff. Funktional ist das völlig ausreichend, führt aber mit fünf wahlfrei ausgewählten Postleitzahlen zu insgesamt sieben SQL-Abfragen. Eines für alle Postleitzahlen und sechs einzelne Statements für die nachzuladenden Städte und Bundesländer. Je nach Daten können es zwischen drei und elf Statements sein, was
die Laufzeit unvorhersehbar macht.

Mit dem vorgestellten „Prefetch Query“ werden zuerst die über Relationen verbundenen Objekte mit der gleichen Einschränkung wie die eigentlich benötigten Objekte in den Entity-Manager geladen und dort zwischengespeichert. Trifft dieser dann später auf die Relation, so ist ihm das Objekt für den Fremdschlüssel schon bekannt und es ist kein weiteres SQL-Statement notwendig. Der „PersistenceContext“ fungiert so als Cache erster Ebene, man muss nur vorab eine weitere kontrollierte Abfrage ausführen.

Zugriffe im Muster von „1+n“ sind so nicht mehr notwendig, im folgenden Beispiel wird „1+n“ zu „1+1“. Die Menge von Postleitzahlen wird immer mit nur zwei Statements geladen, völlig unabhängig davon, ob die Postleitzahlen in der gleichen Stadt in einem Bundesland oder in verschiedenen Städten in verschiedenen Bundesländern liegen:

SELECT e.city.state FROM PostalCode e WHERE e.code IN (:codes)
SELECT e FROM PostalCode e JOIN FETCH e.city WHERE e.code IN (:codes)

In der ersten Abfrage werden die benötigten Bundesländer geladen, wobei der EntityManager die Ergebnisobjekte nur in seinem Cache speichert. In der zweiten Query werden bewusst ein JOIN über die „n:1“-Beziehung gemacht und gleichzeitig Städte und Postleitzahlen geladen. Damit duplizieren sich zwar Städte in der Ergebnismenge, das reale Verhältnis der Objekte zueinander ist jedoch nur „1,3:1“, weshalb das unproblematisch ist. Drei einzelne Abfragen wären an dieser Stelle auch denkbar. Der gezeigte Code führt im Ergebnis zu einer deterministischen Anzahl von Statements (immer exakt zwei) und damit zu sehr stabilen Ausführungszeiten.

Problematische „…ToMany“-Relationen

Im Gegensatz zu „…ToOne“- sind „…ToMany“-Beziehungen schon im Standard „lazy“, die mit der Entität gelieferte Collection enthält also im ersten Schritt keine Daten und führt erst beim Zugriff auf die Elemente oder der Größe zu einem zusätzlichen SQL-Statement. „Eager“-Loading solcher Relationen führt ohnehin aufgrund der kartesischen Produkte zu großen Ergebnismengen und ist daher nur zu empfehlen, wenn sich die Daten-Konstellation im statistischen Mittel „1:1“ annähert; sonst hilft hier eventuell auch wieder ein „Prefetch Query“ der Daten als gesonderte Abfrage.

Weitere Lese-Optimierungen

Sind die Lesezugriffe auf diese Art optimiert, gibt es noch weitere Wege, die Zugriffe zu beschleunigen:

  • Das Lesen aus der Datenbank mithilfe der Aufrufe „query.setFirstResult(…)“ sowie „query.setMaxResults(…)“ ermöglicht die Reduktion der Gesamtmenge und auch das Lesen eines beliebigen Teils eines größeren Ergebnisses. Oft werden nicht alle Daten benötigt und die Datenbank dadurch entlastet.
  • Für Lesezugriffe auf Entitäten mit vielen Attributen kann man auch kleiner geschnittene DTO-Objekte mit selektiv ausgewählten Spalten als Inhalt erhalten. JPQL bietet dazu die „constructor_expression“ in der „SELECT_CLAUSE“. Die geringere Daten-Anforderung an die Datenbank kann beispielsweise mit Indizes auch hinsichtlich I/O optimiert werden.
  • Auch die Anbindung von Stored-Procedures hat sich seit JPA 2.1 verbessert und ermöglicht es, komplexe Funktionen in der Datenbank unterzubringen sowie das Ergebnis in wenigen Interaktionen zu lesen, siehe „NamedStoredProcedureQuery“.

Updates und Löschungen

Bei Updates hat man weniger Kontrolle über die Interaktion mit der Datenbank, da der Entity-Manager die bekannten Objekte am Ende der Transaktion selbst synchronisiert. Batch-Optimierung wird dabei schon genutzt, sofern der Parameter dafür gesetzt ist. Bei einer sehr großen Anzahl von Objekten kann das Erkennen veränderter Objekte allerdings auch eine teure Operation sein.

Die Standard-Strategie ist oft ein Vergleich mit dem Zustand zum Lade-Zeitpunkt. Bei vielen Objekten ist dies speicher- und laufzeitintensiv und tritt an verschiedenen Stellen einer Transaktion auf, in jedem Fall aber am Ende der Transaktion. Der Code, der hier ausgeführt wird, wird zur Laufzeit von einem entsprechenden Methoden-Interceptor hinzugefügt und ist zur Entwicklungszeit nicht ersichtlich.

Daher ist das Erkennen und Zuordnen dieser Laufzeit nicht immer einfach. Mit implementierungsspezifischen Bytecode-Instrumentierungen kann diese Überprüfung der Objekte auf Veränderungen jedoch optimiert werden, etwa zu einer „DirtyFlag“-Strategie. Damit können in der Transaktion geladene Objekte performant aktualisiert werden, oft erhält man allerdings ein verändertes Objekt von der Oberfläche zurück und muss dieses wieder mit der Datenbank zusammenführen, der sogenannte

Merge

Ein Merge vereinfacht die Aufgabe der Aktualisierung von Datenbank-Inhalten, wenn die Objekte außerhalb der Transaktion verändert wurden und anschließend gespeichert werden sollen:

City cityFromUI = …
City mergedCity = em.merge(cityFromUI);

Sofern ein Objekt in der Transaktion noch nicht geladen wurde, führt der Merge-Aufruf ein „SELECT“-Statement auf den Primärschlüssel aus und kopiert alle Werte der übergebenen Instanz in das neu geladene Objekt. Man erhält das zukünftig vom Entity-Manager beachtete Objekt zurück. Weitere Änderungen müssen auf diesem Ergebnis-Objekt durchgeführt werden. Trotzdem führt die Aktualisierung auf diese Art immer zu mehreren Zugriffen, genauso wie im manuellen Fall. Durch das Laden des Datensatzes tritt auch in diesem Szenario das „1+n“-Problem auf, wenn das Objekt geladen werden muss. Im Code der Anwendung zeigt sich ein vergleichbares Problem, wenn eine Relation verändert werden soll; man benötigt eine Instanz des neuen bezogenen Objekts. Ist der technische Schlüssel bekannt, so gibt es hier eine Abkürzung:

City city = em.getReference(City.class, 4711L);
postalCode.setCity(city);

Die hier erzeugte Instanz der City ist ein leerer Proxy, der nur den Primär-Schlüssel kennt. Für das Herstellen der Relation genügt dies, weil man sich hier auf die referenzielle Integrität der Datenbank verlassen kann. Greift man aber auf die Attribute dieses Objekts zu, so wird es im Hintergrund nachgeladen.

JPQL liefert zusätzlich die direkte Möglichkeit eines Update-Statements, was die maximale Optimierung des Zugriffs zur Aktualisierung ist, wenn man das Objekt aus der Datenbank selbst nicht unbedingt benötigt.

Mit der „CASCADE“-Option kann man sich auf den ersten Blick Code und Zeit sparen, weil der Entity-Manager den Objekt-Graphen durchläuft. Leider bricht das damit einhergehende Wechseln der Entitäten auch eine mögliche „Batch-Sortierung“. Zudem besteht die Gefahr, dass man irgendwann nicht mehr überblicken kann, wie groß die Menge der zu betrachtenden Objekte wirklich ist. Schlimmstenfalls werden diese für den „CASCADE“ auch nachgeladen. Daher kann hier nur der vorsichtige und eingeschränkte Einsatz angeraten werden.

Caching

Caching ist eine Möglichkeit, Zugriffe auf die Datenbank größtenteils zu vermeiden. Leider ist dies in einem Cluster keine einfache Aufgabe, daher müssen die betroffenen Daten folgende Kriterien erfüllen:

  • Sie werden häufig gelesen
  • und selten, besser nie verändert
  • Veraltete Daten sind für die Anwendung akzeptabel
  • Der Speicherverbrauch ist kein Problem

Sind diese Kriterien erfüllt, ist es gut und einfach einsetzbar. Für Daten, die sich häufig verändern, bedeutet Caching allerdings erhöhte Komplexität, die im Projektteam bewältigt werden muss. Daher sollten die bisher genannten Optimierungen ausgeschöpft worden sein, bevor man über Caching-Strategien nachdenkt.

Ab in die Wolke

Cloud-Provider bieten zu guten Bedingungen ganze Plattformen für Anwendungen an, auch Datenbank-Instanzen können mit wenigen Mausklicks bereitgestellt werden. Aus den genannten Gründen der Latenz-Problematik ist eine alleinige Verlagerung der Datenbank in die Cloud keine gute Idee. Die Anwendung sollte ebenfalls umgezogen werden.

In den Rechenzentren beziehungsweise Regionen werden Latenz-Zeiten erreicht, die für normale Anwendungsfälle mehr als ausreichend sind. Messungen des Autors ergaben beispielsweise ein bis zwei Millisekunden für einen Datenbank-Roundtrip innerhalb einer Region. Allerdings ist dann meist die Ausfallsicherheit nicht mehr gegeben.

Das zweite Datacenter sollte man betrieblich getrennt, aber trotzdem in der Nähe wählen. Da Licht in Glas “nur” noch 200.000 km/s schnell ist, benötigt ein Ping-Roundtrip zwischen Amerika und Europa mindestens 80 Millisekunden. Der zu Anfang auf unter zwei Sekunden Laufzeit optimierte Anwendungsfall läuft in einer
Region mit ein bis zwei Millisekunden Latenz etwa sechzehn Sekunden.

Ohne Optimierung sind die Laufzeiten in jedem dieser Infrastruktur- Szenarien inakzeptabel hoch. Die Begründung dafür liegt darin, dass die Latenz nicht nur eine einmalige Auswirkung auf die Netzwerk-Performance hat, sondern auch die Bandbreite der Übertragung mindert. Vor allem im Kontext der Cloud gibt man die Verantwortung, aber auch die Kontrolle über die Infrastruktur größtenteils ab. Daher ist darauf zu achten, dass das Code-Design den Anforderungen und Randbedingungen der Infrastruktur entspricht.

JDBC und der Treiber

Im oben genannten Szenario mit rund hundert Millisekunden Latenz bei der Kommunikation zwischen Amerika und Europa wurden plötzlich selbst in der optimierten Code-Variante bis zu 44 Minuten Laufzeit gemessen. Eine Laufzeit, die sich mit normalen Mitteln nicht mehr einfach erklären lies. Also wurde der Netzwerkverkehr analysiert und es hat sich gezeigt, dass es durchaus Sinn macht, sich mit den speziellen Optionen des JDBC-Treibers auseinanderzusetzen. Im Falle des MySQL-Treibers werden Batch-JDBC-Aufrufe trotz entsprechender Nutzung via Hibernate als einzelne Statements übertragen, das Szenario war also wieder bei der ursprünglichen Anzahl Statement-Roundtrips angekommen – nur jetzt multipliziert mit 100ms.

Dieses Beispiel betont einmal mehr, wie wichtig eine deterministische, nicht von den Daten abhängige Anzahl SQL-Statements zur Datenbank sein kann. Hier lag der Grund im JDBC-Treiber und wurde durch Zusatzoptionen behoben. Andere Treiber – andere Probleme: der Oracle-Treiber beispielsweise macht in der Standardeinstellung beim Lesen von Daten je 10 Zeilen einen Roundtrip, was beim Lesen von größeren Datenmengen deutlich auf die Bremse drücken kann.

Fazit

JPA ist für Java-Anwendungen ein essenzielles Framework und es funktioniert auch größtenteils erwartungsgemäß, problemlos und äußerst elegant. Viele Statements für eine Transaktion müssen kein Problem darstellen, wenn deren Anzahl stabil bleibt.

Kritische Transaktionen und Anwendungen, die viele Statements erzeugen, kann man im Nachgang mit wenig Aufwand optimieren. Zusätzlicher Code mit expliziten Anweisungen ermöglicht dedizierte Verbesserungen an kritischen Stellen und ist daher wartungsfreundlicher als generische Lösungen, die kleine Veränderungen schwierig umsetzbar machen. Lazy-Annotationen mit dedizierten Queries mit FETCH JOIN oder entsprechenden Entity-Graphen sind hier ein guter Ansatz.

Jegliches Tuning sollte jedoch nach der funktionalen Umsetzung erfolgen, da schon kleine fachliche Änderungen das Szenario der Zugriffe grundlegend verändern können.
Den Entwicklern sollten die konkrete Infrastruktur und die notwendigen Randbedingungen immer bewusst sein, damit die richtigen Code-Bausteine optimiert werden können. Eine einfache Maßnahme, wie die Aktivierung der Konsolenausgabe der laufenden SQL-Befehle in der Entwicklungsumgebung, kann hier schon Wunder bewirken.

Kenntnisse über die tatsächlichen Verhältnisse der Relationen helfen bei der Optimierung der JOINs und damit der Ergebnismengen. Nicht ganz am Ende steht natürlich die Optimierung der Datenbank für die optimierten Zugriffe, damit diese nicht doch an deren Performance scheitern.

Short URL for this post: https://wp.me/p4nxik-3qv
This entry was posted in Java Persistence and tagged , . Bookmark the permalink.

Leave a Reply