JPA & Hibernate: 5 Performance-Fehler, die fast jedes Projekt macht
JPA ist nicht langsam — falscher Einsatz schon
Hibernate und JPA haben einen schlechten Ruf in Sachen Performance. Aber in den meisten Fällen liegt das Problem nicht beim Framework, sondern bei typischen Anwendungsfehlern, die sich leicht vermeiden lassen.
Hier sind die fünf häufigsten Performance-Killer — und ihre Lösung.
1. Das N+1 Query Problem
Der Klassiker. Sie laden eine Liste von Bestellungen, und für jede Bestellung wird eine separate Query für den Kunden ausgeführt.
Das Problem:
// 1 Query für Bestellungen + N Queries für Kunden
List<Bestellung> bestellungen = em.createQuery(
"SELECT b FROM Bestellung b", Bestellung.class)
.getResultList();
for (Bestellung b : bestellungen) {
System.out.println(b.getKunde().getName()); // Lazy Load!
}
Die Lösung: JOIN FETCH
List<Bestellung> bestellungen = em.createQuery(
"SELECT b FROM Bestellung b JOIN FETCH b.kunde",
Bestellung.class)
.getResultList();
Eine Query statt N+1. Bei 1.000 Bestellungen ist das der Unterschied zwischen 1 und 1.001 Datenbankaufrufen.
2. Eager Loading als Default
@ManyToOne ist standardmäßig EAGER — das bedeutet, jede Relation wird sofort mitgeladen, auch wenn Sie die Daten gar nicht brauchen.
Die Lösung:
@ManyToOne(fetch = FetchType.LAZY)
private Kunde kunde;
Faustregel: Setzen Sie alles auf LAZY und laden Sie gezielt per JOIN FETCH oder EntityGraph, wenn Sie die Daten brauchen.
3. Fehlende Projektionen
Sie brauchen nur Name und E-Mail, laden aber die komplette Entity mit 30 Feldern, LOBs und Relationen.
Das Problem:
// Lädt ALLES — auch Felder, die Sie nicht brauchen
List<Kunde> kunden = em.createQuery(
"SELECT k FROM Kunde k", Kunde.class)
.getResultList();
Die Lösung: DTO-Projektionen
record KundeDto(String name, String email) {}
List<KundeDto> kunden = em.createQuery(
"SELECT new at.cgs.dto.KundeDto(k.name, k.email) FROM Kunde k",
KundeDto.class)
.getResultList();
Weniger Daten über die Leitung, kein Dirty Checking, kein Managed State — deutlich schneller.
4. Batch-Operationen ohne Batching
1.000 Entities einzeln persistieren erzeugt 1.000 INSERT-Statements.
Die Lösung:
# application.properties (Quarkus/Spring)
hibernate.jdbc.batch_size=25
hibernate.order_inserts=true
hibernate.order_updates=true
for (int i = 0; i < entities.size(); i++) {
em.persist(entities.get(i));
if (i % 25 == 0) {
em.flush();
em.clear(); // First-Level Cache leeren!
}
}
5. Kein Query-Monitoring
Der gefährlichste Fehler: Sie wissen gar nicht, welche Queries Hibernate generiert.
Aktivieren Sie SQL-Logging in der Entwicklung:
# Quarkus
quarkus.hibernate-orm.log.sql=true
# Spring Boot
spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql=TRACE
Für Produktion: Nutzen Sie Hibernate Statistics oder ein APM-Tool (z.B. OpenTelemetry) um langsame Queries zu identifizieren.
Bonus: EntityGraph statt JOIN FETCH
Für flexible Ladingstrategien ohne Query-Anpassung:
@NamedEntityGraph(
name = "Bestellung.mitKunde",
attributeNodes = @NamedAttributeNode("kunde")
)
@Entity
public class Bestellung { ... }
// Verwendung
EntityGraph<?> graph = em.getEntityGraph("Bestellung.mitKunde");
List<Bestellung> result = em.createQuery("SELECT b FROM Bestellung b")
.setHint("jakarta.persistence.fetchgraph", graph)
.getResultList();
Fazit
JPA und Hibernate sind mächtige Werkzeuge — aber sie verzeihen Unwissenheit nicht. Die gute Nachricht: Die fünf häufigsten Fehler sind leicht zu erkennen und zu beheben. Aktivieren Sie SQL-Logging, prüfen Sie Ihre Fetch-Strategien, und nutzen Sie Projektionen wo möglich.
In unseren Training-Angeboten behandeln wir JPA-Performance als festen Bestandteil der Java-Entwickler-Ausbildung.
