CodeGym /Kurse /Hibernate deep-dive /Hibernate Statistics: nützliche Zähler

Hibernate Statistics: nützliche Zähler

Hibernate deep-dive
Level 26, Lektion 1
Verfügbar

1. Die Rolle von Statistics neben dem SQL-Log

Wenn das SQL-Log eine Videoaufzeichnung von Überwachungskameras ist, dann sind Hibernate Statistics die Zähler am Eingang: „Wie viele Leute sind reingekommen“, „Wie oft wurde die Tür geöffnet“, „Wie oft ist der Wachmann Kaffee holen gelaufen“. Das Video liefert Details und Kontext, aber es ist schwierig, damit schnell zwei Durchläufe eines Szenarios zu vergleichen. Die Zähler erzählen nicht die ganze Geschichte, liefern aber ein kompaktes Bild, das ohne Rätselraten wiederholt und verglichen werden kann.

Das SQL-Log hat das Problem bereits gezeigt. Jetzt möchte man das Ausmaß verstehen: Ist es eine einzelne schwere Abfrage, eine Serie von Secondary Selects oder einfach das Gefühl, dass „es irgendwie zu viel ist“. Genau hier braucht man die Zähler.

Praktisch sieht das so aus: Sie führen denselben Read-Case (z. B. „Katalogseite mit aktiven Produkten“) in zwei Implementierungsvarianten aus. Das SQL-Log sagt Ihnen, was ausgeführt wurde, und die Statistics antworten, wie oft und in welchem Umfang. Und dieses „Wie oft“ erfasst sehr schnell die klassischen Probleme: N+1, unerwartete Nachladevorgänge von Kollektionen, Materialisierung eines zu großen Graphen und sogar Fälle, in denen beim „Lesen“ plötzlich ein flush auftritt.

Ein sehr wichtiger Punkt: Statistics ersetzen nicht das SQL-Log. Sie sind wie eine kurze Zusammenfassung nach dem Lesen eines Romans. Wenn der Roman schlecht ist, wird die Zusammenfassung so aussehen: „Viele Seiten, wenig Sinn“. Aber wenn Sie ein Audit der Datenebene durchführen, brauchen Sie trotzdem die Zusammenfassung – sonst müssen Sie jedes Mal aufs Neue das „Video“ durchgehen.

2. Einschalten von Statistics per Profil

Hibernate Statistics sind nicht aus einem einfachen Grund standardmäßig eingeschaltet: Das Sammeln von Metriken kostet etwas. Es fügt jeder Abfrage und vielen internen ORM-Operationen einen kleinen Overhead hinzu. In einem Lernprojekt ist das in Ordnung – wir lernen ja gerade, zu messen. In einem Produktionsdienst möchte man die Statistik normalerweise nicht dauerhaft eingeschaltet lassen: Es ist zu leicht, das System ein bisschen langsamer zu machen, „nur damit die Diagramme hübsch aussehen“.

Im Commerce Persistence Lab leben wir in der Disziplin der Profile. Daher ist der typische und korrekte Weg, die Statistik mit einem separaten Profil stats zu aktivieren, damit die Anwendung standardmäßig „normal“ ist und sich für die Diagnose in ein Labor verwandelt.

Eine minimale Konfiguration in application-stats.yml könnte so aussehen:

spring:
  jpa:
    properties:
      hibernate:
        # Hibernate-Statistikerfassung einschalten (sonst sind alle Zähler null)
        generate_statistics: true

Diese Einstellung allein schaltet die Erfassung der Statistik ein. Wie Sie sie dann auslesen, entscheiden Sie selbst: manuell über Statistics im Code (das ist heute unser Hauptweg) oder auch über einen Logger, falls Sie eine Zusammenfassung in den Logs sehen möchten. Aber der Logger ist eine heikle Sache: Sie bekommen viel Text, aber wir brauchen jetzt genau den „Schnitt“ von fünf Zahlen für ein einziges Szenario.

Und ja, es ist normal, dass die Anwendung mit aktiviertem Profil stats bei jeder Abfrage etwas „schwerer“ wird. Das ist jetzt kein Fehler und kein „Hibernate bremst schon wieder“, sondern der bewusste Preis für das Mikroskop: Wenn Sie eine Messung einschalten, zahlen Sie immer einen kleinen Preis für die Messung selbst.

3. Zugriff auf Statistics in Spring Boot

Bisher haben wir viel „Hibernate“ gesagt, aber der Code läuft trotzdem in Spring Boot, und an der Oberfläche haben wir JPA (EntityManager, EntityManagerFactory). Daher die logische Frage: Wo in diesem Schichtkuchen versteckt sich das Objekt, das uns die Statistik liefert?

Die Antwort ist einfach: Statistics leben in der Hibernate SessionFactory. In einer Spring-Boot-Anwendung mit JPA haben wir normalerweise eine EntityManagerFactory, und diese kann man mit unwrap(...) wieder auf die Hibernate-Ebene „herunterbrechen“. Das ist einer der wenigen Fälle, in denen wir bewusst unter die JPA-Abstraktion gehen – weil wir eine Hibernate-spezifische Diagnose benötigen.

Der direkteste Weg (überall dort, wo eine EntityManagerFactory verfügbar ist) sieht so aus:

import jakarta.persistence.EntityManagerFactory;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;

// Von der JPA-Abstraktion nach Hibernate „hinabsteigen“, um Zugriff auf die Statistik zu bekommen
Statistics statistics = entityManagerFactory
        .unwrap(SessionFactory.class) // Hibernate SessionFactory holen
        .getStatistics();             // Das Statistik-Objekt holen (Zähler für die gesamte SessionFactory)

Um diesen Code nicht im ganzen Projekt zu duplizieren, ist es praktischer, ihn einmal als Spring Bean zu konfigurieren und dann einfach Statistics in die benötigten Services zu injecten. Das ist keine „zwingende Architektur“, sondern eine Lerninfrastruktur, die die Laborübungen kürzer und verständlicher macht.

Beispielkonfiguration:

import jakarta.persistence.EntityManagerFactory;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HibernateStatsConfig {

    @Bean
    Statistics hibernateStatistics(EntityManagerFactory emf) {
        // Statistics als Bean bereitstellen, um unwrap(...) nicht im ganzen Projekt zu wiederholen
        return emf.unwrap(SessionFactory.class).getStatistics();
    }
}

Jetzt kann man an jeder Stelle des Projekts (z. B. in com.example.commerce.labsupport) einfach Statistics im Konstruktor entgegennehmen. Ja, das ist ein „globales“ Objekt für die gesamte SessionFactory. Und genau deshalb werden wir gleich immer wieder betonen: Wir messen ein einzelnes Szenario, setzen die Zähler vor der Messung zurück und tun das nicht im mehrthredigen Kampf „alle optimieren gleichzeitig alles“.

4. Snapshot erstellen ohne Selbstbetrug

Hibernate Statistics haben eine tückische Eigenschaft: Sie wirken einfach, und deshalb werden sie sehr leicht falsch verwendet. Der häufigste Anfängerfehler ist, die Zähler „wie sie sind“ anzusehen und Schlüsse zu ziehen. Das ist ungefähr so, als würde man die Geschwindigkeit eines Autos messen, ohne den Tageskilometerzähler zurückgesetzt zu haben und ohne zu klären, ob man überhaupt gefahren ist oder mit laufendem Motor auf dem Parkplatz stand.

Das richtige Ritual (ja, das ist der seltene Fall, wo ein Ritual nützlich ist) sieht so aus: Sie wählen einen einzigen Read-Case oder Write-Case aus, setzen die Zähler zurück, führen das Szenario aus, lesen die Werte aus und vergleichen sie dann mit einer anderen Implementierungsvariante. Wenn Sie kein clear() machen, landet in Ihrer Messung alles: die Abfragen beim Start der Anwendung, Migrationslogs, zufällige Hintergrundnachladevorgänge und sogar das, was Sie gerade im benachbarten IDE-Tab gemacht haben, während Sie „nur mal schnell geprüft haben“.

Ein minimaler „Schnitt“ für ein Szenario könnte so aussehen:

import org.hibernate.stat.Statistics;

// Wichtig: Zähler vor der Messung des konkreten Szenarios zurücksetzen
statistics.clear();
catalogQueryService.findActiveProducts(); // ein einzelner konkreter Use Case

// Werte sofort nach Ausführung des Szenarios auslesen
System.out.println("queries=" + statistics.getQueryExecutionCount());      // queries=1
System.out.println("statements=" + statistics.getPrepareStatementCount()); // statements=1

Beachten Sie eine feine Nuance: Wir versuchen nicht, „die ganze Anwendung“ zu messen. Wir messen „einen einzigen Use Case“. Im Idealfall sollten Sie das Szenario zu diesem Zeitpunkt klar benennen können, wie ein Mensch: „Katalogseite mit aktiven Produkten“, „Detailabruf einer Bestellung mit Positionen“, „Bestelltabelle mit Status NEW“. Wenn Sie das Szenario nicht benennen können, werden Sie auch Ihre Zahlen nur schwer interpretieren können.

Und noch ein wichtiges Detail: Selbst ein Read-Case kann unerwartet einen flush auslösen (das haben wir bereits im Thema Flush und Dirty Checking besprochen). Daher schauen wir heute nicht nur auf die Abfragen, sondern behalten im Hinterkopf: „Gab es einen Schreibvorgang, wo wir ihn nicht erwartet haben?“

5. Fünf Schlüsselzähler

Statistics können sehr viel, aber gerade deshalb ertrinken Anfänger schnell in Zahlen und beginnen entweder, das Werkzeug zu ignorieren, oder machen im Gegenteil eine Religion daraus („die Metrik hat 7 gesagt, also ist es schlecht“). Wir gehen es erwachsen an: Wir wählen ein minimales Set, das den maximalen Nutzen für die Diagnose der Persistenzebene im Rahmen dieses Kurses bringt.

Heute interessieren uns fünf Zähler: queryExecutionCount, prepareStatementCount, entityLoadCount, collectionFetchCount, flushCount. Sie decken gut die drei Hauptfragen der Diagnose ab: Wie viel query-basierte Aktivität ist durch das ORM gelaufen, wie viel SQL-Arbeit ist tatsächlich auf JDBC-Ebene angefallen, wie viele Daten wurden als verwaltete Objekte materialisiert, und hat es einen versteckten Schreibvorgang (flush) in einem Szenario gegeben, das „rein lesend“ sein sollte.

Zur Veranschaulichung eine kompakte Tabelle, die man gut im Kopf behalten kann:

Metrik (Statistics) Was sie im Alltag zählt Welche Symptome sie am schnellsten erfasst
getQueryExecutionCount() Wie viele Abfragen durch den Hibernate-Query-Mechanismus gelaufen sind; kein universeller total-SQL-Zähler Schnelles Signal für Chatty-Query-Aktivität und N+1; zusammen mit SQL-Log, getPrepareStatementCount() und Fetch-Zählern lesen
getPrepareStatementCount() Wie viele JDBC PreparedStatement wurden vorbereitet (annähernd: wie viele SQL Statements tatsächlich gingen) Reales SQL-Arbeitsvolumen, einschließlich unerwarteter UPDATE
getEntityLoadCount() Wie viele Entity-Instanzen wurden geladen Overfetching und „zu viele Entitäten geholt“
getCollectionFetchCount() Wie oft Hibernate Kollektionen (to-many) nachgeladen hat Lazy Collections, sekundäre SELECT, N+1 bei Kollektionen
getFlushCount() Wie oft ein flush stattgefunden hat „Im Read-Case gibt es einen Schreibvorgang“, Flush-before-Query, versteckte Änderungen

Im Folgenden besprechen wir jede Metrik einzeln, aber mit dem wichtigen Hinweis: Wir lesen sie nicht einzeln. Eine einzelne Zahl gibt selten die richtige Diagnose. Die Diagnose entsteht meist aus der Kombination von zwei oder drei Kennzahlen plus den Beobachtungen im SQL-Log.

queryExecutionCount: Schnelles Signal für ein Chatty-Szenario

Wenn ein Entwickler zum ersten Mal das SQL-Log einschaltet, erlebt er oft eine seltsame Mischung aus Gefühlen: Es ist zwar nützlich, aber nach einer Minute wollen die Augen schon zufallen und in den Urlaub fahren. queryExecutionCount ist eine Möglichkeit, schnell festzustellen, ob ein Szenario in ein Chatty-Query-basiertes Verhalten abdriftet. Aber es ist wichtig, ihn nicht zu überschätzen: Es ist kein universeller Zähler für die gesamte Hibernate-SQL-Aktivität.

Stellen wir uns ein einfaches Katalogszenario vor: Wir möchten eine Liste aktiver Produkte abrufen. Wir machen eine einzige JPQL-Abfrage – und erwarten eine einzige Datenbankabfrage. Dann sollte queryExecutionCount im idealen Basis-Fall gleich 1 sein (oder ungefähr so, falls das Szenario noch einige zwingende Service-Abfragen enthält, aber im Labor versuchen wir, das Rauschen zu vermeiden).

Mini-Beispiel einer Messung:

// Metrik streng für ein einziges Szenario erfassen
statistics.clear();
catalogQueryService.findActiveProducts();

// Sollte in einem „gesunden“ Szenario nahe 1 sein (eine root query)
System.out.println(statistics.getQueryExecutionCount()); // 1

Wenn Sie jedoch List<Product> zurückgeben und dann im Code darüber iterieren und product.getDetails() oder product.getAssignments() aufrufen, wird das Szenario chatty. Das wird man fast sicher im SQL-Log sehen, und quantitativ wird es oft besser durch einen Anstieg von prepareStatementCount, collectionFetchCount und anderen Fetch-spezifischen Zählern erfasst. queryExecutionCount bleibt hier ein nützliches schnelles Signal, reicht aber allein nicht mehr aus.

prepareStatementCount: Wie viel SQL ist tatsächlich geflossen

Nach der ersten Bekanntschaft mit queryExecutionCount möchte der Anfänger oft glücklich leben: „Oh, ich hab's verstanden, wir reduzieren einfach die Anzahl der Abfragen, dann wird's schnell.“ Und genau hier bringt uns prepareStatementCount sanft auf den Boden der Tatsachen zurück. Denn Abfragen, Vorbereitung von Statements und die tatsächliche SQL-Arbeit sind verwandte, aber nicht identische Dinge.

prepareStatementCount ist näher an der JDBC-Realität: Wie oft musste Hibernate SQL-Statements zur Ausführung vorbereiten. Wenn Sie beispielsweise in einem „lesenden“ Szenario versehentlich eine verwaltete Entität geändert haben (im Stil von setName(trim())), kann Hibernate beim Flush ein UPDATE senden, und der prepareStatementCount steigt. Dabei kann queryExecutionCount klein bleiben, denn ein UPDATE ist keine „Query“ im JPQL-Sinne, aber es ist trotzdem echte Datenbankarbeit.

Mini-Beispiel, wo prepareStatementCount versteckte Arbeit aufdeckt:

// Zähler zurücksetzen, bevor wir die „Reinheit“ des Read-Case prüfen
statistics.clear();
catalogQueryService.findAndAccidentallyMutateProductName(productId);

// „Queries“ und „echte Statements“ vergleichen: UPDATE kann nur im zweiten auftauchen
System.out.println("queries=" + statistics.getQueryExecutionCount());      // queries=1
System.out.println("statements=" + statistics.getPrepareStatementCount()); // statements=2

Die Zahl „2“ ist hier nicht absolut wichtig. Wichtig ist, dass sie größer ist, als man für reines Lesen erwarten würde. Das ist ein Signal: „Schau dir das SQL-Log an – da ist offensichtlich mehr als nur Selects.“ Und genau das brauchen wir im Audit: schnell eine unerwartete Schreiboperation bemerken.

entityLoadCount: Wie viele Entitäten wurden geladen

Es gibt eine sehr beliebte Falle, besonders nach der Einführung von JOIN FETCH. Der Entwickler macht „eine Abfrage“ und freut sich: queryExecutionCount = 1. Aber dann ist die Anwendung trotzdem schwer, und der Memory-Footprint scheint groß. Warum? Weil „eine Abfrage“ sehr viele Daten bringen kann, die Hibernate in verwaltete Entity-Objekte verwandelt.

entityLoadCount zeigt, wie viele Entity-Instanzen geladen wurden. Es ist nicht die „Größe des Result Sets in Zeilen“ und nicht die „Anzahl der Spalten“. Es ist genau: „Wie viele Entitäten sind als verwaltete Objekte im Persistenzkontext aufgetaucht“. Für die Diagnose ist das extrem praktisch: Sie können sehen, dass ein Read-Case ein „ganzes Universum von Entitäten“ mitzieht, obwohl Sie geschäftlich nur eine Liste von 20 Zeilen mit vier Spalten brauchten.

Mini-Beispiel des Vergleichs zweier Ansätze für dasselbe Szenario (konzeptionell):

// In diesem Szenario interessiert uns genau das Volumen der materialisierten Entitäten
statistics.clear();
catalogQueryService.findCatalogPageWithEntities();

// Wenn die Zahl groß ist, ist das ein direkter Hinweis auf Overfetching, selbst wenn die Abfrage formal eine einzige ist
System.out.println("entities=" + statistics.getEntityLoadCount()); // entities=240

Wenn Sie sehen, dass für eine „einfache Katalogseite“ 240 Entitäten geladen wurden, ist das ein Grund zum Nachdenken: Wollten Sie wirklich so viele Objekte materialisieren? Oder sollte dieses Szenario projection-basiert und „schlank“ sein? Wir streiten nicht erneut über „Entity vs. Projection“ (das war früher), aber die Statistik hilft, den Preis einer Entscheidung in Zahlen zu sehen.

collectionFetchCount: Nachladen von Kollektionen

Beim Lazy-Loading von to-many-Beziehungen gibt es einen kniffligen Effekt: Sie können ehrlich eine einzige Root-Abfrage machen, eine Liste von Bestellungen erhalten, und dann irgendwo „auf dem Weg“ order.getItems().size() aufrufen oder in einer Schleife über die items gehen. Für Java-Code sieht das harmlos aus, aber für Hibernate ist es das Signal „Initialisiere die Kollektion“, also führe zusätzliche Abfragen aus.

collectionFetchCount zeigt, wie oft solche Kollektionen nachgeladen wurden. Für die Praxis ist das eine geradezu goldene Metrik: Wenn Sie eine Liste von Bestellungen lesen und sehen collectionFetchCount > 0, dann haben Sie irgendwo eine to-many-Kollektion lazy angesprochen, und das führt fast garantiert entweder zu N+1 oder zu einer Reihe von sekundären Selects.

Stellen wir uns ein Szenario mit einer Bestellliste vor:

// Prüfen, ob beim Durchlaufen des Ergebnisses lazy to-many-Kollektionen „schießen“
statistics.clear();
orderQueryService.findRecentOrdersAndTouchItems();

// Jeder Wert > 0 ist ein Grund, im SQL-Log nach sekundären SELECTs zu suchen
System.out.println("collections=" + statistics.getCollectionFetchCount()); // collections=30

Die Zahl „30“ im Kommentar ist nur eine Veranschaulichung: Normalerweise entspricht sie ungefähr der Anzahl der Bestellungen in der Liste, weil jede Bestellung ihre eigene Kollektion separat nachlädt. Und dann gehen Sie ins SQL-Log und sehen ein sich wiederholendes select ... from order_item where order_id=? viele Male. Statistics arbeiten hier wie ein „Rauchmelder“: Sie erklären nicht das Feuer im Detail, sagen aber „Es raucht, geh hin und schau, woher“.

flushCount: Alarmzeichen für einen Read-Case

Flush ist ein Thema, das bereits vielen Leuten die Laune verdorben hat, weil es „früher als erwartet“ auftreten kann. Aber in der Diagnose ist flushCount ein sehr dankbarer Indikator. Besonders wenn wir einen Read-Case messen.

Wenn Sie ein Leseszenario analysieren, ist Ihr idealer flushCount normalerweise 0. Er kann größer als null werden, wenn Sie innerhalb der Transaktion etwas geändert haben (auch versehentlich), oder wenn Hibernate gezwungen war, den Kontext zu synchronisieren, um die Korrektheit einer Abfrage sicherzustellen. In jedem Fall ist es ein Signal: „Vorsicht, das Szenario ist nicht rein lesend“.

Mini-Beispiel:

// Read-Case: Sinnvollerweise sollte es hier keine Synchronisation (Flush) mit der Datenbank geben
statistics.clear();
orderQueryService.findOrdersReadCase();

// Ideal: 0. Wenn > 0, suchen wir nach accidental update / flush-before-query
System.out.println("flushes=" + statistics.getFlushCount()); // flushes=0

Wenn hier plötzlich flushes=1 steht, müssen Sie nicht in Panik verfallen und „Hibernate reparieren“. Öffnen Sie ruhig das SQL-Log und verstehen Sie, was den Flush verursacht hat. Oft ist es ein banaler accidental update, weil „wir die Zeile leicht korrigiert haben“, „einen Standardwert gesetzt haben“, „ein Embeddable berührt haben“ oder „ein Listener ausgelöst wurde“. Wir gehen heute nicht auf Callbacks ein, aber ein Flush-Signal in einem Read-Case ist immer ein Grund zu prüfen, ob Ihre Grenze zwischen Lesen und Schreiben nicht beschädigt wurde.

6. Metrik-Kombinationen: Diagnostische Muster

Nach der Besprechung der fünf Zähler stellt sich die logische Frage: „Okay, was gilt als ‚gut‘ und was als ‚schlecht‘?“. Die Antwort ist nicht universell, aber es gibt einfache Muster, die in der realen Entwicklung helfen, in einer Minute zu verstehen, in welche Richtung man weiter graben muss. Das ist ähnlich wie in der Medizin auf dem Niveau „Temperatur und Blutdruck“: Sie stellen keine Diagnose, aber sie helfen sehr bei der Wahl des nächsten Schritts.

Hier sind einige typische Kombinationen in Tabellenform (als Hinweis, nicht als Naturgesetz):

Was Sie in den Metriken sehen Was das oft bedeutet Was Sie als Nächstes tun (im Rahmen der Diagnose)
queryExecutionCount und/oder prepareStatementCount deutlich höher als erwartet, collectionFetchCount signifikant N+1 oder sekundäre Selects aufgrund von to-many SQL-Log öffnen und nach sich wiederholenden Abfragen mit order_id / product_id suchen
queryExecutionCount klein, aber entityLoadCount riesig Eine Abfrage zieht zu viele Entitäten (Overfetching) Prüfen, warum der Read-Case einen Entity-Graphen statt eines schlanken Read-Modells liest
flushCount > 0 in einem Read-Case Im Leseszenario gibt es einen Schreib-/Änderungsvorgang oder Flush-before-Query Nach accidental update, unerwarteten UPDATEs im SQL, unbeabsichtigten Änderungen suchen
prepareStatementCount höher als sinngemäß erwartet Im Szenario gibt es „überflüssiges SQL“ (nicht nur Selects) oder zusätzliche Service-Operationen Mit dem SQL-Log abgleichen: Welche Statements kamen hinzu

Beachten Sie, dass uns diese Tabelle noch nicht automatisch zu EXPLAIN drängt. Wenn die Zähler ein Chatty-ORM-Verhalten zeigen – viele Abfragen, Kollektionsnachladungen, überflüssige Entity-Loads –, wird das Problem zunächst auf der Ebene der Leseform behandelt. Zu generiertem SQL, EXPLAIN und dem PostgreSQL-Plan sollte man erst gehen, wenn die Anzahl der Abfragen bereits gering ist, aber die Schwere in einem einzigen konkreten SQL steckt.

Wichtig: Wir machen hier bewusst keine „Optimierungs-Checkliste“. Es sind genau „Diagnosemuster“. Der Sinn des Tages ist nicht, jetzt sofort alles um jeden Preis zu beschleunigen, sondern sagen zu können: „Das Problem sieht aus wie N+1“ oder „Das Problem sieht aus wie Overfetching“, und dann gezielt das SQL zu öffnen und die Hypothese zu bestätigen.

Um die Idee des „Vergleichs zweier Varianten“ zu festigen, kann man sich gedanklich ein Experiment vorstellen: Eine Katalogimplementierung gibt Entitäten zurück, die andere eine Projection. Wir schreiben hier keinen riesigen Code, sondern nehmen zwei aufeinanderfolgende Schnitte:

// Zwei Implementierungsvarianten „auf sauberen Zählern“ vergleichen
statistics.clear();
catalogQueryService.findCatalogPageWithEntities();
long entitiesQueries = statistics.getQueryExecutionCount();

statistics.clear();
catalogQueryService.findCatalogPageWithProjection();
long projectionQueries = statistics.getQueryExecutionCount();

// Nur einen Indikator ausgeben, um schnell den Unterschied im ORM-Verhaltensmuster zu sehen
System.out.println("entitiesQueries=" + entitiesQueries);     // entitiesQueries=18
System.out.println("projectionQueries=" + projectionQueries); // projectionQueries=2

Das ist kein „Performance-Nachweis“. Es ist ein Nachweis der Verhaltensform des ORM: In der ersten Variante gibt es deutlich mehr Abfragen. Dann öffnen Sie den SQL-Trace und schauen, warum es so viele sind (oft findet man dort genau den besagten sich wiederholenden Secondary Select).

7. Mini-Infrastruktur für den Snapshot

Wenn Sie den zehnten Messdurchlauf am Tag machen, wird das separate Ausgeben von fünf Zahlen mühsam. An dieser Stelle reicht ein kleiner lokaler Snapshot der Zähler. Kein neues „Projektgerüst“, sondern einfach eine kompakte Verpackung einer einzelnen Messung nach einem konkreten Szenario.

Zum Beispiel kann man direkt neben dem Experiment einen solchen record halten:

// Lokaler Snapshot der Zähler für eine einzelne Messung
public record StatisticsSlice(
        long queryCount,
        long prepareStatementCount,
        long entityLoadCount,
        long collectionFetchCount,
        long flushCount
) {
}

Das reicht bereits aus, um nach dem Szenario die Werte zu holen und in einer Zeile auszugeben:

statistics.clear();
catalogQueryService.findActiveProducts();

StatisticsSlice slice = new StatisticsSlice(
        statistics.getQueryExecutionCount(),
        statistics.getPrepareStatementCount(),
        statistics.getEntityLoadCount(),
        statistics.getCollectionFetchCount(),
        statistics.getFlushCount()
);

System.out.println(slice); // StatisticsSlice[queryCount=1, prepareStatementCount=1, entityLoadCount=20, collectionFetchCount=0, flushCount=0]

Die einzige Sicherheitsregel, die man unbedingt aussprechen sollte: statistics.clear() löscht die Zähler global für die gesamte SessionFactory. Daher darf man das in einer echten mehrthredigen Anwendung nicht „einfach so“ machen – Sie würden die Zahlen für andere Szenarien löschen. Aber in unserem Labor messen wir lokal, isoliert, und deshalb ist dieser Trick angemessen.

8. Typische Fehler bei Hibernate Statistics

Fehler Nr. 1: Statistik ist ausgeschaltet, Sie messen „Leere“.
Das ärgerlichste Szenario sieht so aus: Sie haben schönen Code für die Messung geschrieben, getQueryExecutionCount() aufgerufen, und er gibt hartnäckig null zurück. Meistens ist die Ursache einfach: hibernate.generate_statistics ist nicht aktiviert (oder das Profil stats ist nicht aktiv). In diesem Moment möchte man Hibernate Herzlosigkeit vorwerfen, aber es ist nicht seine Schuld: Der Zähler ist ausgeschaltet – der Zähler zählt nicht.

Fehler Nr. 2: statistics.clear() vergessen und die „Zahlen des gesamten Lebens“ bekommen.
Wenn Sie die Zähler nicht vor jeder Messung zurücksetzen, erhalten Sie nicht die Statistik des Szenarios, sondern alles, was seit dem Start der Anwendung passiert ist. Da landen Migrationen, Seed-Daten, Ihre zufälligen Abfragen und sogar das, was der benachbarte Test gemacht hat. Solche Zahlen lassen sich nicht ehrlich miteinander vergleichen, weil Sie zwei verschiedene „Lebenskontexte“ vergleichen.

Fehler Nr. 3: Nicht einen Use Case gemessen, sondern „ungefähr irgendwas“.
Statistics sind nur dann nützlich, wenn Sie sagen können: „Genau diese Methode / dieses Szenario messe ich jetzt“. Wenn Sie innerhalb der Messung mehrere Services aufrufen, dann noch eine Abfrage „für den Fall der Fälle“ machen, dann Entitäten loggen und versehentlich Kollektionen initialisieren, wird der Snapshot zu einem Brei. Versuchen Sie, ein Szenario in möglichst reiner Form zu messen.

Fehler Nr. 4: Einer einzelnen Zahl glauben und die anderen ignorieren.
Manchmal sieht ein Entwickler queryExecutionCount=1 und feiert den Sieg, während der entityLoadCount ins Unermessliche gestiegen ist. Oder umgekehrt: Er ist stolz, den entityLoadCount reduziert zu haben, hat aber versehentlich den collectionFetchCount erhöht. Die ORM-Diagnose ist fast immer mehrdimensional: Man muss auf das Zusammenspiel der Metriken achten, nicht auf eine einzelne.

Fehler Nr. 5: Zwei Läufe unter verschiedenen Bedingungen und mit unterschiedlichen Daten vergleichen.
Wenn Sie im ersten Messdurchlauf 20 Produkte haben und im zweiten 2000 Produkte (weil Sie big-dataset aktiviert haben), dann liegen die Unterschiede in den Zählern nicht nur an Ihrem Code. Das sind schlicht verschiedene Szenarien. Fixieren Sie für den Vergleich die Eingabeparameter: Seitengröße, Filter, Status, und nach Möglichkeit – denselben Datensatz.

1
Aufgabe
Hibernate deep-dive, Level 26, Lektion 1
Gesperrt
Minimaler Snapshot der Hibernate-Statistiken
Minimaler Snapshot der Hibernate-Statistiken
1
Aufgabe
Hibernate deep-dive, Level 26, Lektion 1
Gesperrt
Vergleich von Entity-Laden und Konstruktorprojektion anhand der Zähler
Vergleich von Entity-Laden und Konstruktorprojektion anhand der Zähler
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION