Woche 5, Tag 5 — JPA Performance und Best Practices
Ziel
Heute willst du praktische JPA Performance und Persistence Best Practices verstehen.
Die Kernfragen:
- Warum kann JPA Performance zum Problem werden?
- Was ist Over-Fetching?
- Was ist Under-Fetching?
- Was ist ein Fetch Plan?
- Wie vermeidest du N+1 Queries?
- Was sind Projections?
- Was sind DTO Projections?
- Was ist Batch Fetching?
- Wie solltest du Repository Methods designen?
- Wie testest du Repositories?
- Was ist
@DataJpaTest? - Was sind typische Persistence Bugs?
1. Kurz-Wiederholung aus Woche 5, Tag 4
In Tag 4 hast du gelernt:
- Entity Relationships mappen Java-Objektreferenzen auf Datenbank-Foreign Keys.
@ManyToOnegehört meist auf die Child-Seite.@OneToMany(mappedBy = "...")repräsentiert meist die inverse Parent Collection.- Die Owning Side hat meist
@JoinColumn. mappedBynutzt Java-Feldnamen, nicht Datenbank-Spaltennamen.- Lazy Loading verzögert das Laden verknüpfter Daten.
- Eager Loading lädt zugehörige Daten sofort.
- „@ManyToOne“ ist standardmäßig eager.
- N+1 bedeutet eine Haupt-Query plus viele zusätzliche Relationship Queries.
- Fetch join, Entity Graph und DTO Projection können helfen, N+1 zu vermeiden.
- REST APIs sollten meist DTOs zurückgeben, nicht Entities.
Merksatz:
Entity relationships are powerful, but they can easily create performance and serialization problems.
Heute lernst du, wie du typische JPA Performance Bugs vermeidest.
2. Warum JPA Performance zum Problem werden kann
JPA ist praktisch, weil du mit Objekten arbeitest.
Aber die Datenbank funktioniert immer noch mit:
tables
rows
columns
joins
indexes
SQL queries
transactions
locks
Probleme treten auf, wenn du vergisst, dass der Objektzugriff SQL auslösen kann.
Beispiel:
task.getClient().getName();
Das sieht aus wie ein einfacher Java-Getter.
Aber wenn der „Client“ lazy ist, kann es Folgendes auslösen:
select * from clients where id = ?
Merksatz:
Mit JPA kann ein einfacher Zugriff auf Java-Eigenschaften Datenbank-Queries auslösen.
3. Die wichtigsten Performance-Probleme
Häufige JPA Performance-Probleme:
N+1 queries
over-fetching too much data
under-fetching and triggering extra queries
loading huge tables without pagination
returning entities directly from APIs
lazy loading during JSON serialization
using eager relationships everywhere
missing indexes
too many writes inside long transactions
external API calls inside transactions
inefficient repository methods
Heutiger Fokus:
fetching
projections
pagination
batch fetching
testing
common bugs
4. Übermäßiges Abrufen
Over-Fetching bedeutet:
Es werden mehr Daten geladen, als der Use Case benötigt.
Beispiel:
API benötigt nur:
{
"id": 1,
"title": "Prepare tax documents"
}
Aber das Repository lädt:
task
client
comments
attachments
audit logs
creator
assignee
Problem:
more SQL work
more memory usage
more network transfer
slower response
more JSON risk if entities are returned
Merksatz:
Over-fetching bedeutet, dass zu viel geladen wird.
5. Under-Fetching
Under-Fetching bedeutet:
Zuerst werden zu wenige Daten geladen und später werden zusätzliche Queries ausgelöst.
Beispiel:
List<TaskEntity> tasks = taskRepository.findByStatus("OPEN");
for (TaskEntity task : tasks) {
System.out.println(task.getClient().getName());
}
Erste Query:
select * from tasks where status = 'OPEN';
Dann eine Query pro Aufgabe:
select * from clients where id = ?;
Dadurch entsteht N+1.
Merksatz:
Under-Fetching führt häufig zu N+1-Queries.
6. Plan abrufen
Ein Fetch Plan bedeutet:
Eine Entscheidung darüber, welche Daten für einen Use Case geladen werden sollen.
Beispielendpunkt:
GET /api/tasks
Vielleicht braucht es:
task id
task title
task status
client name
Es braucht nicht:
client address
client invoices
task comments
attachments
audit logs
Guter Fetch Plan:
Load only task fields and client name.
Merksatz:
Plan abrufen bedeutet, genau das zu laden, was der Use Case benötigt.
7. Beispiel für einen fehlerhaften Fetch Plan
Schlechtes Entitysdesign:
@ManyToOne(fetch = FetchType.EAGER)
private ClientEntity client;
@OneToMany(mappedBy = "task", fetch = FetchType.EAGER)
private List<CommentEntity> comments;
Problem:
Every task query loads client and comments even when not needed.
Dadurch entsteht:
large SQL queries
duplicate rows
more memory usage
performance surprises
Besser:
@ManyToOne(fetch = FetchType.LAZY)
private ClientEntity client;
@OneToMany(mappedBy = "task")
private List<CommentEntity> comments = new ArrayList<>();
Dann bei Bedarf explizit abrufen.
Merksatz:
Bevorzugen du Lazy-Beziehungen und explizites Abrufen für jeden Use Case.
8. Drei gängige Fetch Strategyn
Um verwandte Daten absichtlich zu laden, verwenden Sie:
fetch join
@EntityGraph
DTO projection
Vergleich:
| Strategie | Gut für |
|---|---|
| Join abrufen | Ladeeinheit plus benötigte Beziehungen |
@EntityGraph | Definieren des Zuordnungsabrufplans für die Repository-Methode |
| DTO Projection | Es werden nur die von der API benötigten Felder geladen |
| Batch Fetching | Reduzierung besonders verzögerter Ladeabfragen |
Merksatz:
Wählen du die Fetch Strategy basierend auf dem Use Case.
9. N+1 mit Fetch Join beheben
Problem:
List<TaskEntity> tasks = taskRepository.findByStatus("OPEN");
return tasks.stream()
.map(task -> new TaskDto(
task.getId(),
task.getTitle(),
task.getClient().getName()
))
.toList();
Potenzial N+1:
1 query for tasks
N queries for clients
Fix:
@Query("""
select t
from TaskEntity t
join fetch t.client
where t.status = :status
""")
List<TaskEntity> findByStatusWithClient(@Param("status") String status);
Nun werden Aufgaben und Clients gemeinsam geladen.
Merksatz:
Fetch Join lädt die Beziehung in derselben Query.
10. „Join“ vs. „Join Fetch“.
Normaler Beitritt:
@Query("""
select t
from TaskEntity t
join t.client c
where c.name = :clientName
""")
List<TaskEntity> findByClientName(@Param("clientName") String clientName);
Dies dient der Filterung.
Join abrufen:
@Query("""
select t
from TaskEntity t
join fetch t.client
where t.status = :status
""")
List<TaskEntity> findByStatusWithClient(@Param("status") String status);
Dadurch wird der „Client“ verbunden und geladen.
Merksatz:
„join“ hilft beim Filtern. „Join Fetch“ lädt auch die Association.
11. Korrigieren du N+1 mit „@EntityGraph“.
Repository:
@EntityGraph(attributePaths = "client")
List<TaskEntity> findByStatus(String status);
Bedeutung:
When finding tasks by status, also fetch client.
Gut, wenn du eine abgeleitete Querymethode beibehalten, aber den Abruf anpassen möchte.
Beispiel:
public interface TaskRepository extends JpaRepository<TaskEntity, Long> {
@EntityGraph(attributePaths = "client")
List<TaskEntity> findByStatus(String status);
}
Merksatz:
„@EntityGraph“ definiert, welche Associations abgerufen werden sollen.
12. Join vs. EntityGraph abrufen
| Thema | Beitreten | abrufen @EntityGraph |
|---|---|---|
| Wo definiert | JPQL-Query | Anmerkung zur Repository-Methode |
| Kontrolle | explizite Query | Plan für Methode abrufen |
| Gut für | benutzerdefiniertes JPQL | abgeleitete Queries mit Abrufanforderungen |
| Lesbarkeit | klare Query | sauberere Methode |
| Risiko | komplexe Paginationsprobleme | Du muss das Abrufen noch verstehen |
Beides ist nützlich.
13. DTO Projection
DTO Projection bedeutet:
Fragen du direkt in einem DTO ab, anstatt vollständige Entities zu laden.
DTO:
public record TaskListDto(
Long id,
String title,
String status,
String clientName
) {
}
Repository:
@Query("""
select new de.klarsync.tasks.TaskListDto(
t.id,
t.title,
t.status,
c.name
)
from TaskEntity t
join t.client c
where t.status = :status
order by t.createdAt desc
""")
List<TaskListDto> findTaskListByStatus(@Param("status") String status);
Dies lädt nur:
task id
task title
task status
client name
Merksatz:
Die DTO Projection lädt nur das, was die API benötigt.
14. Warum die DTO Projection schneller sein kann
Das Laden der Entity kann Folgendes laden:
all entity columns
entity state tracking
relationships if fetched
persistence context management
DTO Projectionslasten:
selected fields only
no managed entity tracking for DTO
API-ready shape
Gut für:
list endpoints
read-only views
dashboards
search results
summary pages
Merksatz:
Für schreibgeschützte Listenendpunkte ist die DTO Projection oft eine gute Option.
15. Schnittstellenprojektion
Spring Data kann auch Schnittstellenprojektionen verwenden.
Projektion:
public interface TaskSummaryView {
Long getId();
String getTitle();
String getStatus();
}
Repository:
List<TaskSummaryView> findByStatus(String status);
Spring Data gibt Objekte zurück, die die Schnittstellenprojektion implementieren.
Dies ist für einfache Teilansichten nützlich.
Merksatz:
Schnittstellenprojektionen machen nur ausgewählte Getter verfügbar.
16. DTO Projection vs. Schnittstellenprojektion
| Thema | DTO Projection | Schnittstellenprojektion |
|---|---|---|
| Geben du | ein Klasse/Datensatz | Schnittstelle |
| Konstrukteur | ja | nein |
| Gut für | API-DTOs löschen | schnelle Teilansichten |
| Querystil | häufig JPQL-Konstruktorausdruck | häufig abgeleitete Queryunterstützung |
| Kontrolle | sehr explizit | praktisch |
Zum Lernen und sauberen API-Design:
DTO records are usually easy to understand.
17. Projektionsfalle
Die Projektion löst nicht automatisch alle Performance-Probleme.
Beispiel:
public interface TaskView {
Long getId();
String getTitle();
ClientView getClient();
}
Verschachtelte Projections können je nach Query und Anbieterverhalten weiterhin Verknüpfungen oder zusätzliches Laden auslösen.
Merksatz für die Prüfung:
Projection helps reduce selected data, but always check generated SQL for important queries.
18. Pagination ist ein Leistungstool
Schlecht:
List<TaskEntity> findAll();
für großen Tisch.
Besser:
Page<TaskEntity> findAll(Pageable pageable);
oder:
Page<TaskListDto> findTaskListByStatus(String status, Pageable pageable);
Warum?
limits rows loaded
reduces memory usage
improves response time
supports UI/API paging
Merksatz:
Große Listenendpunkte sollten Pagination verwenden.
19. Seiten- vs. Slice-Leistung
Page<T> ergibt:
content
total elements
total pages
page number
page size
Normalerweise ist jedoch eine Zählabfrage erforderlich.
Slice<T> ergibt:
content
whether next slice exists
Es ist keine Gesamtzahl erforderlich.
Verwenden du „Slice“, wenn:
infinite scroll
load more button
no need for total pages
Merksatz:
„Seite“ hat Gesamtwerte. „Slice“ kann leichter sein.
20. Join mit Paginationsfalle abrufen
Gefährlich:
@Query("""
select c
from ClientEntity c
join fetch c.tasks
""")
Page<ClientEntity> findAllWithTasks(Pageable pageable);
Problem:
one client can have many tasks
join creates duplicate client rows
pagination may become incorrect or inefficient
Sicherere Optionen:
use DTO projection
fetch IDs first, then fetch relationships
use batch fetching
avoid collection fetch join with pagination
use EntityGraph carefully
design endpoint differently
Merksatz:
Seien du vorsichtig bei der Pagination von Queries, die Join-Sammlungen abrufen.
21. Batch Fetching
Der Batch Fetching ist eine Hibernate-Optimierung.
Einfache Definition:
Batch Fetching lädt mehrere Lazy-Associations stapelweise statt einzeln.
Ohne Batch Fetching:
1 query for tasks
1 query per client
Beim Batch Fetching kann Hibernate Clients in Gruppen laden:
select * from clients where id in (?, ?, ?, ?, ?);
Dadurch werden viele kleine Queries reduziert.
Merksatz:
Batch Fetching reduziert N+1 durch Gruppieren von Lazy Loads.
22. Globale Batch Fetchinggröße
Beispielkonfiguration:
spring:
jpa:
properties:
hibernate.default_batch_fetch_size: 50
Bedeutung:
Hibernate can batch lazy association loading up to 50 IDs at a time.
Dies ersetzt kein gutes Query Design.
Es handelt sich um eine Sicherheits-/Leistungsoptimierung.
Merksatz:
Batch Fetching hilft, aber explizite Abrufpläne sind immer noch wichtig.
23. @BatchSize
Hibernate-spezifische Anmerkung:
@BatchSize(size = 50)
@ManyToOne(fetch = FetchType.LAZY)
private ClientEntity client;
oder bei Abholung:
@BatchSize(size = 50)
@OneToMany(mappedBy = "client")
private List<TaskEntity> tasks = new ArrayList<>();
Dadurch wird Hibernate angewiesen, diese Zuordnung stapelweise zu laden.
Hinweis:
@BatchSize is Hibernate-specific, not pure JPA.
Merksatz:
„@BatchSize“ ist ein Hibernate-spezifischer Batch-Ladehinweis.
24. Batch Fetching vs. fetch join
| Thema | Batch Fetching | Beitreten | abrufen | ------------ | --------------------- | ---------------- | | Wie | Gruppen Lazy Loads | lädt Association in derselben Query | | Gut für | Reduzierung von N+1 weltweit | spezifische Use Caseabfrage | | Anzahl der Queries | weniger Anfragen | oft eine Query | | Kontrolle | weniger explizit | explizit | | Portabilität | Ruhezustandsspezifisch | JPQL/JPA-Konzept | | Zusammen verwenden | möglich | möglichst sorgfältig |
Merksatz:
fetch join ist explizit. Batch Fetching ist eine nützliche Backup-Optimierung.
25. Wählen du nur das aus, was du brauchen
Endpunkt der fehlerhaften Liste:
List<TaskEntity> findByStatus(String status);
Dann werden viele Felder und Beziehungen abgebildet.
Besserer Listenendpunkt:
@Query("""
select new de.klarsync.tasks.TaskListDto(
t.id,
t.title,
t.status,
c.name
)
from TaskEntity t
join t.client c
where t.status = :status
""")
Page<TaskListDto> findTaskList(
@Param("status") String status,
Pageable pageable
);
Merksatz:
Listenendpunkte benötigen normalerweise zusammenfassende Daten, keine vollständigen Entitysdiagramme.
26. Verwenden du indexfreundliche Queries
Dies ist eher datenbankseitig, aber wichtig.
Query:
where status = ?
order by created_at desc
Ein nützlicher Index könnte sein:
create index idx_tasks_status_created_at
on tasks(status, created_at desc);
JPA kann fehlende Datenbankindizes nicht auf magische Weise reparieren.
Merksatz:
Gute JPA-Queries benötigen weiterhin gute Datenbankindizes.
27. Vermeide Funktionen für indizierte Spalten
Weniger indexfreundlich:
where lower(title) like lower('%tax%')
Kann bei großen Tischen langsamer sein.
Optionen:
use database-specific text indexes
use normalized search column
use full-text search
use proper search engine for complex search
Für normale einfache Apps ist „ContainingIgnoreCase“ in Ordnung.
Für große Datenmengen muss die Suche gestaltet werden.
Merksatz:
Einfache Suchmethoden eignen sich gut für kleine Datenmengen, für große Suchvorgänge ist jedoch eine Indexstrategie erforderlich.
28. Vermeide „findAll()“ in echten APIs
Schlecht:
@GetMapping("/api/tasks")
public List<TaskDto> list() {
return taskRepository.findAll()
.stream()
.map(this::toDto)
.toList();
}
Probleme:
loads entire table
slow response
high memory usage
hard to scale
Besser:
@GetMapping("/api/tasks")
public Page<TaskDto> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
return taskService.list(page, size);
}
Merksatz:
Öffentliche Listen-APIs sollten normalerweise paginiert sein.
29. Halte die Transactions kurz
Schlecht:
@Transactional
public void createTask(CreateTaskRequest request) {
TaskEntity task = taskRepository.save(new TaskEntity(request.title()));
externalNotificationApi.send(task.getId());
fileStorage.upload(request.file());
}
Problem:
transaction stays open during slow network calls
database locks/resources held longer
external side effect may succeed but DB transaction may roll back
Besser:
save data in transaction
commit
publish event
process external side effect after commit
use outbox pattern for reliability
Merksatz:
Halte Datenbanktransaktionen kurz und vermeiden du langsame externe Aufrufe innerhalb der Datenbank.
30. Vermeide Lazy Loading während der JSON-Serialisierung
Schlecht:
@GetMapping("/api/clients/{id}")
public ClientEntity getClient(@PathVariable Long id) {
return clientRepository.findById(id).orElseThrow();
}
Jackson kann während der Serialisierung auf Lazy-Beziehungen zugreifen.
Probleme:
LazyInitializationException
N+1 during serialization
huge response
infinite recursion
sensitive fields leaked
Besser:
@GetMapping("/api/clients/{id}")
public ClientDetailDto getClient(@PathVariable Long id) {
return clientService.getClientDetail(id);
}
Merksatz:
Ordnen du Entities DTOs zu, bevor du von den Controllern zurückkehren.
31. Sitzung in Ansicht öffnen
„Sitzung in Ansicht öffnen“ kann den Persistencekontext während des Renderns von Webanforderungen offen halten.
Dies kann Lazy-Loading-Fehler verbergen.
Aber es kann Folgendes verursachen:
SQL queries during JSON serialization
hidden N+1 problems
unclear transaction boundaries
performance surprises
Gute Praxis:
fetch needed data in service
map to DTO in service
return DTO
do not rely on serialization-time lazy loading
Merksatz:
Verwenden du Open Session in View nicht als Designstrategie.
32. Repository-Methodendesign
Gute Repository-Methoden sind:
specific
readable
use entity property names
return correct type
support pagination for large data
avoid very long derived names
use @Query when clearer
Gut:
Page<TaskEntity> findByClientIdAndStatus(
Long clientId,
String status,
Pageable pageable
);
Zu lang:
List<TaskEntity> findByClientIdAndStatusAndPriorityAndDueDateBeforeAndTitleContainingIgnoreCaseOrderByCreatedAtDesc(
Long clientId,
String status,
String priority,
LocalDate dueDate,
String keyword
);
Besser:
@Query("""
select t
from TaskEntity t
where t.clientId = :clientId
and t.status = :status
and t.priority = :priority
and t.dueDate < :dueDate
and lower(t.title) like lower(concat('%', :keyword, '%'))
order by t.createdAt desc
""")
List<TaskEntity> searchTasks(...);
Merksatz:
Abgeleitete Queries sind solange gültig, bis der Methodenname nicht mehr lesbar ist.
33. Best Practices für Repository-Rückgabetypen
Verwendung:
Optional<T> for zero or one result
List<T> for small/controlled many results
Page<T> for paginated results with totals
Slice<T> for load-more results without totals
boolean for exists queries
long for count queries
DTO projection for read-only API views
Beispiele:
Optional<UserEntity> findByEmail(String email);
boolean existsByEmail(String email);
long countByStatus(String status);
Page<TaskListDto> findTaskList(..., Pageable pageable);
34. Verwenden du „existsBy...“ anstelle von „Loading Entity“.
Schlecht:
Optional<UserEntity> user = userRepository.findByEmail(email);
if (user.isPresent()) {
throw new EmailAlreadyUsedException(email);
}
Besser:
if (userRepository.existsByEmail(email)) {
throw new EmailAlreadyUsedException(email);
}
Warum?
database only checks existence
no need to load full entity
clear intent
Merksatz:
Verwenden du „existsBy...“, wenn du nur wissen muss, ob eine Zeile existiert.
35. Verwenden du Count vorsichtig
Auf großen Tischen kann das Zählen teuer sein.
Beispiel:
long total = taskRepository.countByStatus("OPEN");
Bei Bedarf ist das in Ordnung.
Vermeide jedoch unnötige Zählabfragen.
Für die Pagination:
Page needs count
Slice can avoid count
Merksatz:
Zählabfragen sind nützlich, aber nicht kostenlos.
36. Massenaktualisierungen und Persistencekontextfalle
Massenaktualisierung:
@Modifying
@Query("update TaskEntity t set t.status = 'DONE' where t.clientId = :clientId")
int completeAllTasksForClient(@Param("clientId") Long clientId);
Wichtig:
Bulk JPQL updates bypass normal dirty checking for already managed entities.
Wenn Entities bereits im Persistencekontext geladen sind, können sie veraltet sein.
Optionen:
@Modifying(clearAutomatically = true, flushAutomatically = true)
Beispiel:
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("update TaskEntity t set t.status = 'DONE' where t.clientId = :clientId")
int completeAllTasksForClient(@Param("clientId") Long clientId);
Merksatz:
Massenaktualisierungen umgehen die Dirty-Prüfung verwalteter Entities und können dazu führen, dass der Persistencekontext veraltet ist.
37. Löschmethoden können gefährlich sein
Gefährlich:
void deleteByClientId(Long clientId);
Dadurch werden möglicherweise viele Zeilen gelöscht.
Besserer Service-Check:
@Transactional
public void deleteClientTasks(Long clientId) {
ClientEntity client = clientRepository.findById(clientId)
.orElseThrow(() -> new ResourceNotFoundException("Client", clientId));
if (!client.canDeleteTasks()) {
throw new BusinessConflictException("Client tasks cannot be deleted");
}
taskRepository.deleteByClientId(clientId);
}
Merksatz:
Massenlöschvorgänge benötigen Unternehmensschutz.
38. Repositories testen
Überprüfung der Repository-Tests:
entity mapping
derived query methods
@Query methods
constraints
persistence behavior
relationships
pagination
sorting
Spring Boot bietet:
@DataJpaTest
Einfache Definition:
„@DataJpaTest“ ist ein fokussierter Testabschnitt für JPA-Komponenten.
Normalerweise wird Folgendes geladen:
entities
Spring Data JPA repositories
JPA-related configuration
test database configuration
Es wird nicht die vollständige Webanwendung geladen.
Merksatz:
„@DataJpaTest“ testet die Persistenceschicht, ohne die gesamte App zu starten.
39. Grundlegendes „@DataJpaTest“.
@DataJpaTest
class TaskRepositoryTest {
@Autowired
private TaskRepository taskRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void findsByStatus() {
TaskEntity task = new TaskEntity("Learn JPA", "OPEN");
entityManager.persistAndFlush(task);
List<TaskEntity> result = taskRepository.findByStatus("OPEN");
assertThat(result).hasSize(1);
assertThat(result.get(0).getTitle()).isEqualTo("Learn JPA");
}
}
Wichtig:
TestEntityManager helps persist test data.
Repository method is tested against database behavior.
40. „@DataJpaTest“-Rollback
Repository-Tests sind üblicherweise transaktional.
Nach jedem Test:
changes are rolled back
Warum nützlich?
tests do not pollute each other
database returns to clean state
Merksatz:
„@DataJpaTest“-Tests führen normalerweise nach jedem Test ein Rollback durch.
41. In-Memory-DB vs. Real-DB
Standardmäßig verwenden viele JPA-Slice-Tests eine eingebettete In-Memory-Datenbank, sofern verfügbar.
Beispiel:
H2
Das geht schnell.
Aber es besteht ein Risiko:
H2 behavior may differ from PostgreSQL/MySQL
native queries may behave differently
database-specific constraints may differ
SQL functions may differ
Verwenden du für ernsthafte Persistencetests denselben Datenbanktyp wie in der Produktion.
Gemeinsame Option:
Testcontainers with PostgreSQL
Merksatz:
Schnelle H2-Tests sind nützlich, aber echte Datenbanktests erkennen echte Datenbankprobleme.
42. Vermeide es, Spring-Daten selbst zu testen
Teste nicht, ob „save()“ funktioniert.
Schlechter Test:
@Test
void saveWorks() {
TaskEntity saved = taskRepository.save(new TaskEntity("A", "OPEN"));
assertThat(saved.getId()).isNotNull();
}
Dies testet hauptsächlich Spring Data/Hibernate.
Bessere Tests:
custom query method works
mapping constraints work
relationship mapping works
pagination returns expected data
native query works
unique constraint behaves correctly
Merksatz:
Teste Ihre Zuordnungen und Queries, nicht das integrierte CRUD von Spring Data.
43. Abgeleitete Queries testen
Repository:
List<TaskEntity> findByClientIdAndStatus(Long clientId, String status);
Test:
@Test
void findsByClientAndStatus() {
TaskEntity openTask = new TaskEntity(10L, "Task A", "OPEN");
TaskEntity doneTask = new TaskEntity(10L, "Task B", "DONE");
TaskEntity otherClientTask = new TaskEntity(20L, "Task C", "OPEN");
entityManager.persist(openTask);
entityManager.persist(doneTask);
entityManager.persist(otherClientTask);
entityManager.flush();
List<TaskEntity> result =
taskRepository.findByClientIdAndStatus(10L, "OPEN");
assertThat(result)
.extracting(TaskEntity::getTitle)
.containsExactly("Task A");
}
44. Pagination testen
@Test
void returnsPagedTasks() {
for (int i = 1; i <= 30; i++) {
entityManager.persist(new TaskEntity("Task " + i, "OPEN"));
}
entityManager.flush();
Pageable pageable = PageRequest.of(0, 10, Sort.by("title"));
Page<TaskEntity> page = taskRepository.findByStatus("OPEN", pageable);
assertThat(page.getContent()).hasSize(10);
assertThat(page.getTotalElements()).isEqualTo(30);
assertThat(page.getNumber()).isEqualTo(0);
}
Dies prüft:
filtering
page size
total count
page number
sorting if asserted
45. Beziehungen testen
Beispiel:
@Test
void taskBelongsToClient() {
ClientEntity client = new ClientEntity("Client A");
entityManager.persist(client);
TaskEntity task = new TaskEntity("Task A", "OPEN", client);
entityManager.persistAndFlush(task);
TaskEntity found = taskRepository.findById(task.getId()).orElseThrow();
assertThat(found.getClient().getName()).isEqualTo("Client A");
}
Dies prüft:
foreign key mapping
relationship mapping
persistence behavior
46. Queryleistung sorgfältig testen
Übertreiben du es bei normalen Unit-Tests nicht mit Leistungstests.
Aber für N+1-sensitiven Code können Sie:
enable SQL logging
inspect query count manually
use Hibernate statistics in integration tests
test specific repository fetch methods
Einfacher praktischer Ansatz:
write repository method with fetch join or projection
review generated SQL
test result correctness
watch SQL logs for unexpected repeated queries
Merksatz:
Prüfe bei leistungsabhängigen Queries das generierte SQL.
47. Häufiger Persistencefehler: Fehlende Transaction
Fehler:
public TaskDto complete(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
return toDto(task);
}
Kein „@Transactional“.
Problem:
dirty checking may not persist change
entity may be detached
behavior depends on repository transaction boundaries
Fix:
@Transactional
public TaskDto complete(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
return toDto(task);
}
Merksatz:
Updates durch Dirty Checking erfordern eine Transaction.
48. Häufiger Persistencefehler: Falsches „mappedBy“.
Falsch:
@OneToMany(mappedBy = "client_id")
private List<TaskEntity> tasks;
Richtig:
@OneToMany(mappedBy = "client")
private List<TaskEntity> tasks;
Warum?
mappedBy uses Java field name, not database column name.
49. Häufiger Persistencefehler: Entity wird von der API zurückgegeben
Schlecht:
@GetMapping("/api/tasks/{id}")
public TaskEntity getTask(@PathVariable Long id) {
return taskRepository.findById(id).orElseThrow();
}
Probleme:
lazy loading during serialization
infinite recursion
sensitive fields exposed
database model becomes API contract
Fix:
@GetMapping("/api/tasks/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id);
}
50. Häufiger Persistencefehler: EAGER überall verwenden
Schlecht:
@ManyToOne(fetch = FetchType.EAGER)
private ClientEntity client;
@OneToMany(fetch = FetchType.EAGER)
private List<CommentEntity> comments;
Problem:
loads too much data
hard to control queries
performance problems
N+1 can still happen
large object graphs
Besser:
@ManyToOne(fetch = FetchType.LAZY)
private ClientEntity client;
@OneToMany(mappedBy = "task")
private List<CommentEntity> comments = new ArrayList<>();
Bei Bedarf explizit abrufen.
51. Häufiger Persistencefehler: ALLES überall kaskadieren
Schlecht:
@ManyToOne(cascade = CascadeType.ALL)
private ClientEntity client;
Problem:
operations on task can cascade to shared client
delete/update danger
unexpected persistence behavior
Besser:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private ClientEntity client;
Merksatz:
Keine Kaskadierung von vielen Kindern auf gemeinsame Eltern.
52. Häufiger Persistencefehler: Lange Transaction
Schlecht:
@Transactional
public void createAndUpload(CreateTaskRequest request) {
TaskEntity task = taskRepository.save(new TaskEntity(request.title()));
cloudStorage.upload(request.file());
emailService.sendTaskCreated(task.getId());
}
Problem:
database transaction remains open during slow external operations
Besser:
save data in transaction
publish event or outbox record
handle upload/email outside transaction
53. Häufiger Persistencefehler: Massenaktualisierung veralteter Entities
Fehler:
@Transactional
public void completeAll(Long clientId) {
List<TaskEntity> tasks = taskRepository.findByClientId(clientId);
taskRepository.completeAllTasksForClient(clientId);
// tasks still may show old status in memory
}
Warum?
bulk update bypasses managed entities already loaded in persistence context
Fixoptionen:
avoid mixing loaded entities and bulk updates
clear persistence context after bulk update
use @Modifying(clearAutomatically = true)
54. Checkliste für praktische Best Practices
Nutzen du diese Checkliste für reale Projekte:
[ ] Controller geben DTOs zurück, nicht Entities.
[ ] Service Layer besitzt Transactions.
[ ] Read Methods nutzen @Transactional(readOnly = true).
[ ] Write Methods nutzen normales @Transactional.
[ ] Große List Endpoints nutzen Pagination.
[ ] Repository Methods nutzen Entity Property Names.
[ ] Lange derived Query Names werden durch @Query ersetzt.
[ ] DTO Projections werden für read-only List Views genutzt.
[ ] Fetch Joins oder EntityGraphs, wenn Relationships gebraucht werden.
[ ] N+1 wird mit SQL Logs geprüft.
[ ] Kein Lazy Loading während JSON-Serialisierung.
[ ] Cascade nur bei klarer Lifecycle Ownership.
[ ] CascadeType.ALL nicht blind nutzen.
[ ] Bulk Updates vorsichtig handhaben.
[ ] Repository Queries mit @DataJpaTest testen.
[ ] Production-ähnliche Database Tests für database-spezifische Queries.
[ ] Transactions kurz halten.
[ ] Keine externen Calls in langen Database Transactions.
55. Typische Prüfungsfallen
Prüfungsfalle 1
JPA Performance hängt stark von der Fetching Strategy ab.
Prüfungsfalle 2
Lazy heißt nicht „nie eine Query“.
Lazy heißt „später laden, wenn zugegriffen wird“.
Prüfungsfalle 3
Eager löst Performance-Probleme nicht automatisch.
Es kann sie verschlimmern.
Prüfungsfalle 4
N+1 kann bei lazy Relationships und auch bei schlechtem eager Fetching passieren.
Prüfungsfalle 5
Fetch join lädt eine Association mit der Haupt-Query.
Prüfungsfalle 6
@EntityGraph kann Associations definieren, die für eine Repository Method gefetcht werden sollen.
Prüfungsfalle 7
DTO Projections können das Laden vollständiger Entities vermeiden.
Prüfungsfalle 8
Pagination ist bei großen Tabellen wichtig.
Prüfungsfalle 9
Page braucht meist die Total Count.
Prüfungsfalle 10
Slice ist leichter, wenn keine Total Count gebraucht wird.
Prüfungsfalle 11
Batch Fetching gruppiert lazy Association Loading.
Prüfungsfalle 12
Batch Fetching ist eine Optimierung, kein Ersatz für gutes Query Design.
Prüfungsfalle 13
@DataJpaTest ist ein JPA-fokussierter Test Slice.
Prüfungsfalle 14
Teste nicht Spring Data CRUD — teste deine Queries und Mappings.
Prüfungsfalle 15
Prüfe generiertes SQL immer für wichtige Queries.
---## 56. Prüfungsfrage: Over-Fetching
Frage:
Was ist Over-Fetching?
Antwort:
Übermäßiges Abrufen bedeutet, dass mehr Daten geladen werden, als der Use Case erfordert, z. B. das Laden vollständiger Entities und Beziehungen, wenn die API nur wenige Felder benötigt.
57. Prüfungsfrage: Under-Fetching
Frage:
Was ist Under-Fetching?
Antwort:
Under-Fetching bedeutet, dass zunächst zu wenig Daten geladen werden und dann später zusätzliche Queries ausgelöst werden, was oft zu N+1-Problemen führt.
58. Prüfungsfrage: Plan abrufen
Frage:
Was ist ein Fetch Plan?
Antwort:
Ein Fetch Plan ist die Entscheidung darüber, welche Entities und Zuordnungen für einen bestimmten Use Case geladen werden sollen.
59. Prüfungsfrage: Fetch Join
Frage:
Was macht ein fetch join?
Antwort:
Ein fetch join lädt die Hauptentität und die angegebene Zuordnung in derselben Query.
60. Prüfungsfrage: „@EntityGraph“.
Frage:
Was macht „@EntityGraph“?
Antwort:
„@EntityGraph“ definiert, welche Zuordnungen für eine Repository-Methode abgerufen werden sollen.
61. Prüfungsfrage: Projektion
Frage:
Was ist eine Projektion in Spring Data JPA?
Antwort:
Eine Projektion ist eine Möglichkeit, nur einen Teil der Daten zurückzugeben, z. B. ausgewählte Felder oder ein DTO, anstatt vollständige Entities zu laden und zurückzugeben.
62. Prüfungsfrage: Batch Fetching
Frage:
Was ist Batch Fetching?
Antwort:
Beim Batch Fetching handelt es sich um eine Hibernate-Optimierung, die das verzögerte Laden mehrerer Entities oder Sammlungen in weniger SQL-Queries gruppiert.
63. Prüfungsfrage: „@DataJpaTest“.
Frage:
Wofür wird „@DataJpaTest“ verwendet?
Antwort:
„@DataJpaTest“ wird verwendet, um JPA-Komponenten wie Entities und Spring Data-Repositorys zu testen, ohne den vollständigen Anwendungskontext zu laden.
64. Prüfungsfrage: Zurückkehrende Entities
Frage:
Warum sollten REST-Controller JPA-Entities normalerweise nicht direkt zurückgeben?
Antwort:
Denn es kann interne Daten offenlegen, verzögertes Laden während der Serialisierung auslösen, N+1-Queries verursachen, eine unendliche Rekursion erzeugen und die API an das Datenbankmodell koppeln.
65. Interview-Antwort
Frage:
Wie vermeiden du N+1-Queries in Spring Data JPA?
Gute Antwort:
Du identifiziere N+1 zunächst, indem du SQL-Protokolle oder die Anzahl der Queries überprüfe. Dann wähle du die richtige Fetch Strategy für den Use Case. Wenn du Entities und eine Beziehung benötige, kannst du einen fetch join oder „@EntityGraph“ verwenden. Wenn der Endpunkt schreibgeschützt ist und nur wenige Felder benötigt, bevorzuge du eine DTO Projection. Durch Batch Fetchingen kann auch die Anzahl der Lazy-Loading-Queries reduziert werden, es ersetzt jedoch kein gutes Query Design.
66. Interview-Antwort
Frage:
Was ist der Unterschied zwischen fetch join, Entity Graph und Projektion?
Gute Antwort:
Ein fetch join ist eine JPQL-Query, die eine Association zusammen mit der Hauptentität lädt. Ein Entitysgraph definiert, welche Associations für eine Repository-Methode abgerufen werden sollen. Eine Projektion gibt nur ausgewählte Daten zurück, oft direkt in ein DTO oder eine Schnittstelle, anstatt einen vollständigen Entitysgraphen zu laden. Für API-Listenansichten sind Projections oft sehr effizient.
67. Interview-Antwort
Frage:
Wie testet man Spring Data JPA-Repositories?
Gute Antwort:
Du verwende „@DataJpaTest“ für Repository-Tests. Es lädt einen fokussierten JPA-Testabschnitt mit Entities und Repositorys anstelle der gesamten Anwendung. Du teste benutzerdefinierte abgeleitete Queries, „@Query“-Methoden, Beziehungszuordnungen, Pagination, Sortierung und Einschränkungen. Für datenbankspezifisches Verhalten oder native Queries bevorzuge du Tests mit demselben Datenbanktyp wie in der Produktion, häufig mit Testcontainern.
68. Interview-Antwort
Frage:
Was sind häufige JPA Performance Bugs?
Gute Antwort:
Zu den häufigsten Fehlern gehören die direkte Rückgabe von Entities von Controllern, die Verwendung von Eager Fetching überall, das Ignorieren von N+1-Queries, das Laden großer Tabellen ohne Pagination, die blinde Verwendung von „CascadeType.ALL“, das Verlassen auf „Open Session in View“, das Durchführen externer Aufrufe innerhalb von Transactions und das Nichtüberprüfen von generiertem SQL auf wichtige Queries.
69. Tiny-Code-Praxis
Repository:
public interface TaskRepository extends JpaRepository<TaskEntity, Long> {
@Query("""
select new de.klarsync.tasks.TaskListDto(
t.id,
t.title,
t.status,
c.name
)
from TaskEntity t
join t.client c
where t.status = :status
order by t.createdAt desc
""")
Page<TaskListDto> findTaskListByStatus(
@Param("status") String status,
Pageable pageable
);
}
Fragen:
- Gibt dies Entities oder DTOs zurück?
- Werden vollständige „TaskEntity“-Objekte geladen?
- Warum ist dies gut für Listenendpunkte?
- Was macht „Pageable“?
- Was solltest du auf Leistung prüfen?
Antworten:
- DTOs.
- Nein, es wählt Felder in „TaskListDto“ aus.
- Es lädt nur das, was der Endpunkt benötigt.
- Es wendet Pagination und Sortierung an.
- Generierte SQL- und Queryanzahl.
70. Tiny Bug-Übung 1
Problem:
@GetMapping("/api/tasks")
public List<TaskEntity> list() {
return taskRepository.findAll();
}
Frage:
Was ist los?
Antwort:
Es gibt Entities direkt zurück und lädt die gesamte Tabelle. Dies kann zu Performance-Problemen, verzögertem Laden während der Serialisierung, Rekursion und Datenlecks führen.
Besser:
@GetMapping("/api/tasks")
public Page<TaskListDto> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
return taskService.list(page, size);
}
71. Tiny Bug-Übung 2
Problem:
@Transactional(readOnly = true)
public List<TaskDto> listOpenTasks() {
List<TaskEntity> tasks = taskRepository.findByStatus("OPEN");
return tasks.stream()
.map(task -> new TaskDto(
task.getId(),
task.getTitle(),
task.getClient().getName()
))
.toList();
}
Frage:
Welcher Leistungsfehler kann auftreten?
Antwort:
N+1. Eine Query lädt Aufgaben, dann kann eine zusätzliche Query den Client jeder Aufgabe laden. Behebung mit fetch join, „@EntityGraph“, DTO Projection oder Batch Fetching.
72. Tiny Bug-Übung 3
Problem:
@ManyToOne(fetch = FetchType.EAGER)
private ClientEntity client;
@OneToMany(fetch = FetchType.EAGER)
private List<CommentEntity> comments;
Frage:
Warum ist das riskant?
Antwort:
Es lädt zugehörige Daten sofort und kann für viele Anwendungsfälle zu viele Daten laden. Dies kann zu großen Queries, hoher Speicherauslastung, doppelten Zeilen und Performance-Problemen führen. Bevorzuge lazy Relationships und explizite Fetch Plans.
Übungsfragen
Frage 1
Was ist Over-Fetching?
Antwort:
Over-Fetching bedeutet, dass mehr Daten geladen werden, als der Use Case benötigt.
Frage 2
Was ist Under-Fetching?
Antwort:
Under-Fetching bedeutet, dass zuerst zu wenig Daten geladen werden und dann später zusätzliche Queries ausgelöst werden, was oft zu N+1 führt.
Frage 3
Was ist ein Fetch Plan?
Antwort:
Ein Fetch Plan ist die Entscheidung, welche Entities, Felder und Associations für einen Use Case geladen werden sollen.
Frage 4
Was ist das N+1-Queryproblem?
Antwort:
N+1 bedeutet, dass eine Query eine Liste lädt und dann für jedes Element eine zusätzliche Query ausgeführt wird, um zugehörige Daten zu laden.
Frage 5
Wie hilft ein fetch join?
Antwort:
Ein fetch join lädt die Hauptentität und die benötigte Zuordnung in derselben Query.
Frage 6
Was macht „@EntityGraph“?
Antwort:
„@EntityGraph“ definiert, welche Zuordnungen für eine Repository-Methode abgerufen werden sollen.
Frage 7
Was ist eine DTO Projection?
Antwort:
Eine DTO Projection ist eine Query, die ausgewählte Daten direkt in ein DTO zurückgibt, anstatt vollständige Entities zu laden.
Frage 8
Was ist eine Schnittstellenprojektion?
Antwort:
Eine Schnittstellenprojektion ist eine Spring Data-Projektionsschnittstelle, die ausgewählte Getter für Teildaten verfügbar macht.
Frage 9
Warum sind Projections für Listenendpunkte nützlich?
Antwort:
du laden nur die Felder, die der Endpunkt benötigt, reduzieren die Speichernutzung, vermeiden unnötige Entitysdiagramme und erstellen API-fähige Daten.
Frage 10
Was ist Batch Fetching?
Antwort:
Beim Batch Fetching handelt es sich um eine Hibernate-Optimierung, die Lazy Loading in weniger SQL-Queries gruppiert.
Frage 11
Ist Batch Fetching ein Ersatz für ein gutes Query Design?
Antwort:
Nein. Der Batch Fetching hilft, die Anzahl der Queries zu reduzieren, aber explizite Abrufpläne und ein gutes Query Design sind immer noch wichtig.
Frage 12
Warum sollten große Listen-APIs Pagination verwenden?
Antwort:
Die Pagination verhindert das Laden großer Tabellen in den Speicher und verbessert die Reaktionszeit und Skalierbarkeit.
Frage 13
Was ist der Unterschied zwischen „Page“ und „Slice“?
Antwort:
„Seite“ umfasst die Gesamtzahl und die Gesamtseitenzahl. „Slice“ ist heller und sagt nur, ob es ein nächstes Slice gibt.
Frage 14
Warum solltest du mit fetch join und Pagination vorsichtig sein?
Antwort:
Sammlungsabrufverknüpfungen können doppelte Zeilen erzeugen und die Pagination falsch oder ineffizient machen.
Frage 15
Warum solltest du es vermeiden, Entities von Controllern zurückzugeben?
Antwort:
Zurückgebende Entities können interne Felder offenlegen, verzögertes Laden während der Serialisierung auslösen, N+1 verursachen, eine unendliche Rekursion erzeugen und die API an das Datenbankmodell koppeln.
Frage 16
Was ist „@DataJpaTest“?
Antwort:
„@DataJpaTest“ ist ein Spring Boot-Test-Slice zum Testen von JPA-Komponenten wie Entities und Repositorys.
Frage 17
Worauf sollten sich Repository-Tests konzentrieren?
Antwort:
Repository-Tests sollten sich auf benutzerdefinierte Queries, abgeleitete Methoden, Zuordnungen, Beziehungen, Einschränkungen, Pagination, Sortierung und datenbankspezifisches Verhalten konzentrieren.
Frage 18
Warum können In-Memory-Datenbanktests riskant sein?
Antwort:
Denn H2 oder eine andere In-Memory-Datenbank kann sich anders verhalten als die Produktionsdatenbank, insbesondere bei nativem SQL, Einschränkungen, Funktionen und Indizes.
Frage 19
Warum sind Massenaktualisierungen im Persistencekontext schwierig?
Antwort:
Massenaktualisierungen umgehen die normale Dirty-Prüfung für bereits verwaltete Entities und können den Persistencekontext mit veralteten Daten hinterlassen.
Frage 20
Was solltest du bei leistungsabhängigen JPA-Queries überprüfen?
Antwort:
Prüfe generiertes SQL, Anzahl der Queries, Indizes, Pagination, Fetch Strategy und ob die Query nur benötigte Daten lädt.
Finale Merksätze
- JPA Performance hängt stark von der Fetching Strategy ab.
- Over-Fetching bedeutet: zu viel laden.
- Under-Fetching verursacht oft N+1.
- Ein Fetch Plan definiert, welche Daten ein Use Case braucht.
- Lazy Loading kann SQL später auslösen.
- Eager Loading kann sofort zu viel laden.
- Fetch join lädt Associations in derselben Query.
@EntityGraphdefiniert zu fetchende Associations.- DTO Projection lädt nur ausgewählte Daten.
- Interface Projection exposed nur ausgewählte Getter.
- Pagination schützt große List Endpoints.
Pagehat Total Count;Sliceist leichter.- Batch Fetching gruppiert lazy Loads.
- Batch Fetching hilft, ersetzt aber kein gutes Query Design.
- Gib Entities nicht aus Controllern zurück.
- Mappe im Service Layer zu DTOs.
- Halte Transactions kurz.
- Keine externen Calls in Transactions.
@DataJpaTestist für fokussierte JPA Repository Tests.- Teste deine Queries und Mappings, nicht Spring Data CRUD selbst.
- Prüfe generiertes SQL immer für wichtige Queries.