Zum Hauptinhalt springen

Woche 5, Tag 3 — Transaktionen im Detail

Ziel

Heute willst du Transaktionen in Spring Data JPA verstehen.

Die Kernfragen:

  1. Was ist eine Transaktion?
  2. Warum brauchen wir Transaktionen?
  3. Was macht @Transactional?
  4. Wo solltest du @Transactional platzieren?
  5. Was ist der Persistence Context?
  6. Was ist Dirty Checking?
  7. Was löst ein Rollback aus?
  8. Was ist der Unterschied zwischen checked und unchecked Exceptions?
  9. Was bedeutet readOnly = true?
  10. Welche typischen Transaktionsfallen gibt es?

1. Kurz-Wiederholung aus Woche 5, Tag 2

In Tag 2 hast du gelernt:

  • Spring Data JPA kann abgeleitete Queries aus Repository-Methodennamen erstellen.
  • Repository-Methodennamen nutzen Entity-Property-Namen, nicht Datenbank-Spaltennamen.
  • @Query kann JPQL- oder native SQL-Queries definieren.
  • JPQL nutzt Entity-Namen und Felder.
  • Native SQL nutzt Tabellennamen und Spalten.
  • Pageable unterstützt Paginierung.
  • Page enthält die Gesamtanzahl.
  • Slice ist leichter als Page.
  • @Modifying wird für Update-/Delete-Queries benötigt.

Merksatz:


Repository-Methoden fragen Entities über Spring Data JPA ab.

Heute lernst du, wie Änderungen committed oder zurückgerollt werden.


2. Was ist eine Transaktion?

Eine Transaktion ist eine Arbeitseinheit.

Einfache Definition:

Eine Transaktion fasst Datenbankoperationen zusammen, sodass sie entweder alle erfolgreich sind oder alle fehlschlagen.

Beispiel:


1. Rechnung erstellen
2. Rechnungspositionen erstellen
3. Kundensaldo aktualisieren
4. Audit-Log schreiben

Wenn Schritt 3 fehlschlägt, willst du normalerweise nicht, dass Schritte 1 und 2 gespeichert bleiben.

Du willst:


alle Operationen erfolgreich

oder:


alle Operationen zurückrollen

Merksatz:

Eine Transaktion schützt die Konsistenz, indem eine Gruppe von Operationen gemeinsam erfolgreich ist oder gemeinsam fehlschlägt.


3. Warum brauchen wir Transaktionen?

Ohne Transaktionen können Daten inkonsistent werden.

Beispiel: Geldüberweisung


1. 100 € von Konto A abziehen
2. 100 € auf Konto B gutschreiben

Wenn Schritt 1 erfolgreich ist, Schritt 2 aber fehlschlägt:


Geld verschwindet

Mit einer Transaktion:


wenn beide Schritte erfolgreich sind -> Commit
wenn ein Schritt fehlschlägt -> Rollback

Merksatz:

Transaktionen schützen die Datenbank vor halbfertigen Änderungen.


4. ACID

Transaktionen werden oft mit ACID beschrieben.

BuchstabeBedeutungEinfache Erklärung
AAtomicityalles oder nichts
CConsistencyDaten bleiben gültig
IIsolationTransaktionen stören sich nicht gegenseitig
DDurabilitycommitted Daten bleiben gespeichert

Für die Zertifizierung ist das wichtigste Wort:


Atomicity = alles oder nichts

5. Was ist @Transactional?

@Transactional sagt Spring:

Führe diese Methode innerhalb einer Transaktion aus.

Beispiel:


@Service
public class TaskService {

private final TaskRepository taskRepository;

public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}

@Transactional
public TaskDto create(CreateTaskRequest request) {
TaskEntity task = new TaskEntity(request.title(), "OPEN");
TaskEntity saved = taskRepository.save(task);
return toDto(saved);
}
}

Spring startet eine Transaktion vor der Methode und committed sie, wenn die Methode erfolgreich ist.

Wenn die Methode mit einer Rollback-auslösenden Exception fehlschlägt, rollt Spring die Transaktion zurück.

Merksatz:

@Transactional erzeugt eine Transaktionsgrenze um eine Methode.


6. Transaktionsgrenze

Transaktionsgrenze bedeutet:


wo die Transaktion startet und wo sie endet

Beispiel:


@Transactional
public void completeTask(Long taskId) {
TaskEntity task = taskRepository.findById(taskId).orElseThrow();
task.complete();
}

Grenze:


Transaktion startet vor completeTask()
Transaktion wird committed, wenn completeTask() erfolgreich zurückkehrt
Transaktion wird zurückgerollt, wenn completeTask() eine Rollback-Exception wirft

Merksatz:

Eine Transaktionsgrenze umschließt eine Arbeitseinheit.


7. Wo sollte @Transactional normalerweise hin?

Normalerweise auf Service-Methoden.

Gut:


@Service
public class TaskService {

@Transactional
public TaskDto completeTask(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));

task.complete();

return toDto(task);
}
}

Warum die Service-Schicht?


Service enthält die Geschäftsoperation
Service kann mehrere Repositories aufrufen
Service definiert die Arbeitseinheit
Controller sollte HTTP-fokussiert bleiben
Repository sollte persistenz-fokussiert bleiben

Merksatz:

Setze Transaktionen um Geschäfts-Use-Cases — normalerweise in der Service-Schicht.


8. Der Controller sollte normalerweise keine Transaktionen definieren

Weniger ideal:


@RestController
public class TaskController {

@Transactional
@PostMapping("/api/tasks/{id}/complete")
public TaskDto complete(@PathVariable Long id) {
return taskService.completeTask(id);
}
}

Besser:


@RestController
public class TaskController {

@PostMapping("/api/tasks/{id}/complete")
public TaskDto complete(@PathVariable Long id) {
return taskService.completeTask(id);
}
}

Service:


@Service
public class TaskService {

@Transactional
public TaskDto completeTask(Long id) {
// business transaction here
}
}

Warum?


Controller verarbeitet HTTP
Service verarbeitet die Geschäftstransaktion

9. Haben Repository-Methoden schon Transaktionen?

Spring-Data-Repository-Methoden haben oft schon Transaktionsverhalten.

Zum Beispiel:


taskRepository.save(task);

kann transaktional laufen.

Aber in echten Apps sollten Service-Methoden trotzdem Transaktionen für Geschäfts-Use-Cases definieren.

Warum?


eine Service-Methode kann mehrere Repository-Methoden aufrufen
alle Operationen sollen gemeinsam committen oder zurückrollen
Service definiert die geschäftliche Arbeitseinheit

Beispiel:


@Transactional
public InvoiceDto createInvoice(CreateInvoiceRequest request) {
ClientEntity client = clientRepository.findById(request.clientId()).orElseThrow();
InvoiceEntity invoice = invoiceRepository.save(new InvoiceEntity(client));
auditLogRepository.save(new AuditLogEntity("INVOICE_CREATED"));
return toDto(invoice);
}

Alle Datenbankänderungen gehören zu einer Transaktion.


10. Was passiert intern mit @Transactional?

Spring nutzt AOP-Proxies.

Vereinfachter Ablauf:


1. Client ruft Service-Methode auf.
2. Aufruf geht durch den Spring-Proxy.
3. Proxy startet Transaktion.
4. Echte Service-Methode läuft.
5. Wenn die Methode normal zurückkehrt, committed der Proxy die Transaktion.
6. Wenn die Methode eine Rollback-Exception wirft, rollt der Proxy die Transaktion zurück.

Merksatz:

@Transactional funktioniert über einen Spring-AOP-Proxy.


11. Beispiel für einen Transaktions-Proxy

Du schreibst:


@Service
public class TaskService {

@Transactional
public void completeTask(Long id) {
// business logic
}
}

Spring erzeugt einen Proxy darum:


TaskService-Proxy

startet Transaktion

ruft echte TaskService.completeTask() auf

committed oder rollt zurück

Deshalb gibt es Fallen — besonders bei Self-Invocation.


12. Self-Invocation-Falle

Schlecht:


@Service
public class TaskService {

public void outerMethod(Long id) {
completeTask(id);
}

@Transactional
public void completeTask(Long id) {
// transactional logic
}
}

Problem:


outerMethod() ruft completeTask() innerhalb derselben Klasse auf.
Der Aufruf geht nicht durch den Spring-Proxy.
@Transactional wird möglicherweise nicht angewendet.

Merksatz:

@Transactional funktioniert, wenn der Aufruf durch den Spring-Proxy geht.

Besser:


@Transactional auf die äußere öffentliche Service-Methode setzen
transaktionale Methode in einen anderen Service verschieben
über einen anderen Spring Bean aufrufen

Beste einfache Lösung:


@Transactional
public void outerMethod(Long id) {
completeTaskInternal(id);
}

private void completeTaskInternal(Long id) {
// logic
}

13. Sichtbarkeits-Falle bei Methoden

@Transactional wird normalerweise auf öffentliche Methoden gesetzt.

Gut:


@Transactional
public void completeTask(Long id) {
}

Riskant/irreführend:


@Transactional
private void completeTask(Long id) {
}

Weil Spring-Proxy-basiertes Transaktionsmanagement normalerweise für von außen aufgerufene Methoden über den Proxy gilt.

Prüfungssicherer Satz:

Setze @Transactional auf öffentliche Service-Methoden, die von außerhalb des Beans aufgerufen werden.


14. Persistence Context

Der Persistence Context ist eines der wichtigsten JPA-Konzepte.

Einfache Definition:

Der Persistence Context ist ein First-Level-Cache und Tracking-Bereich für managed Entities innerhalb einer Transaktion.

Wenn JPA eine Entity innerhalb einer Transaktion lädt, wird die Entity managed.

Beispiel:


@Transactional
public TaskDto completeTask(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();

task.complete();

return toDto(task);
}

Innerhalb der Transaktion:


task wird vom Persistence Context verwaltet

JPA trackt Änderungen daran.

Merksatz:

Der Persistence Context trackt managed Entities.


15. Managed Entity

Eine managed Entity ist eine Entity, die aktuell vom Persistence Context getrackt wird.

Beispiel:


TaskEntity task = taskRepository.findById(id).orElseThrow();

Innerhalb einer Transaktion ist diese Entity normalerweise managed.

Wenn du sie änderst:


task.complete();

merkt JPA die Änderung.

Beim Commit kann JPA die Datenbank aktualisieren.

Merksatz:

Änderungen an managed Entities können beim Commit automatisch gespeichert werden.


16. Dirty Checking

Dirty Checking bedeutet:

JPA erkennt automatisch Änderungen an managed Entities und aktualisiert die Datenbank beim Flush/Commit.

Beispiel:


@Transactional
public void completeTask(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
}

Kein expliziter save()-Aufruf.

Trotzdem kann die Datenbank aktualisiert werden, weil:


task ist managed
JPA trackt Änderungen
Transaktion wird committed
Dirty Checking erkennt die Änderung
SQL-Update wird ausgeführt

Merksatz:

Dirty Checking speichert Änderungen an managed Entities, ohne save() erneut aufzurufen.


17. Dirty-Checking-Beispiel

Entity:


@Entity
@Table(name = "tasks")
public class TaskEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String status;

public void complete() {
this.status = "DONE";
}
}

Service:


@Transactional
public void completeTask(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));

task.complete();
}

Mögliches SQL beim Commit:


update tasks set status = 'DONE' where id = ?

Obwohl du nicht aufgerufen hast:


taskRepository.save(task);

18. Brauche ich save() nach dem Aktualisieren einer managed Entity?

Innerhalb einer Transaktion:


@Transactional
public void updateTitle(Long id, String title) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.changeTitle(title);
}

Normalerweise ist kein save() nötig.

Warum?


task ist managed
Dirty Checking trackt Änderungen
Commit flushed die Änderungen

Aber für neue Entities:


TaskEntity task = new TaskEntity("New task", "OPEN");
taskRepository.save(task);

Du brauchst save(), um die neue Entity zu persistieren.

Merksatz:

Neue Entity braucht save(). Bestehende managed Entity kann über Dirty Checking aktualisiert werden.


19. Flush

Flush bedeutet:

Änderungen im Persistence Context mit der Datenbank synchronisieren.

Flush kann passieren:


vor dem Transaktions-Commit
vor bestimmten Queries
beim Aufruf von flush()
wenn repository saveAndFlush() genutzt wird

Wichtig:


Flush sendet SQL an die Datenbank, aber Commit macht es endgültig.

Merksatz:

Flush sendet Änderungen. Commit macht Änderungen endgültig.


20. Commit vs. Rollback

Commit:


Transaktionsänderungen dauerhaft machen

Rollback:


Transaktionsänderungen rückgängig machen

Beispiel:


@Transactional
public void createTaskAndFail() {
taskRepository.save(new TaskEntity("A", "OPEN"));
throw new RuntimeException("fail");
}

Weil eine Runtime Exception geworfen wird:


Transaktion wird zurückgerollt
Task wird nicht dauerhaft gespeichert

21. Standard-Rollback-Regeln

Standardmäßig rollt Spring zurück bei:


RuntimeException
Error

Standardmäßig rollt Spring nicht zurück bei:


checked Exception

Rollback-Beispiel:


@Transactional
public void createTask() {
taskRepository.save(new TaskEntity("A", "OPEN"));
throw new IllegalStateException("Something failed");
}

IllegalStateException ist eine RuntimeException.

Ergebnis:


Rollback

Merksatz:

Standardmäßig rollen Runtime Exceptions zurück; checked Exceptions nicht.


22. Checked vs. unchecked Exceptions

Unchecked Exception:


RuntimeException

Beispiele:


IllegalArgumentException
IllegalStateException
NullPointerException
ResourceNotFoundException, wenn sie RuntimeException erweitert

Checked Exception:


Exception

aber nicht RuntimeException.

Beispiel:


IOException

Checked Exceptions müssen normalerweise deklariert oder gefangen werden.


23. Checked-Exception-Falle

Beispiel:


@Transactional
public void importTasks() throws IOException {
taskRepository.save(new TaskEntity("A", "OPEN"));

throw new IOException("file failed");
}

Standard-Ergebnis:


Transaktion kann committen

Warum?


IOException ist eine checked Exception.
Spring rollt checked Exceptions standardmäßig nicht zurück.

Lösung:


@Transactional(rollbackFor = IOException.class)
public void importTasks() throws IOException {
taskRepository.save(new TaskEntity("A", "OPEN"));
throw new IOException("file failed");
}

Merksatz:

Nutze rollbackFor, wenn eine checked Exception zurückrollen soll.


24. rollbackFor

Beispiel:


@Transactional(rollbackFor = Exception.class)
public void runImport() throws Exception {
// database changes
throw new Exception("fail");
}

Das sagt Spring:


auch für Exception zurückrollen

Spezifischer ist oft besser:


@Transactional(rollbackFor = ImportFailedException.class)

Vermeide es, alles zu breit zu machen, ohne nachzudenken.


25. noRollbackFor

Manchmal willst du kein Rollback für eine bestimmte Exception.

Beispiel:


@Transactional(noRollbackFor = NotificationFailedException.class)
public void createTaskAndNotify() {
taskRepository.save(new TaskEntity("A", "OPEN"));

notificationService.send();

// if notification fails, maybe task should still be saved
}

Bedeutung:


Nicht zurückrollen bei NotificationFailedException.

Vorsichtig einsetzen.

Merksatz:

rollbackFor fügt Rollback-Regeln hinzu. noRollbackFor schließt Rollback-Regeln aus.


26. Exception-Fangen-Falle

Schlecht:


@Transactional
public void createTask() {
try {
taskRepository.save(new TaskEntity("A", "OPEN"));
externalCall();
} catch (RuntimeException ex) {
log.warn("Ignored error", ex);
}
}

Problem:


Exception wird gefangen und nicht erneut geworfen
Methode kehrt normal zurück
Spring committed die Transaktion

Wenn du Rollback willst, musst du:


die Exception erneut werfen
oder die Transaktion als rollback-only markieren

Besser:


@Transactional
public void createTask() {
taskRepository.save(new TaskEntity("A", "OPEN"));
externalCall();
}

oder:


@Transactional
public void createTask() {
try {
taskRepository.save(new TaskEntity("A", "OPEN"));
externalCall();
} catch (RuntimeException ex) {
log.warn("Error", ex);
throw ex;
}
}

Merksatz:

Wenn du die Exception fängst und verschluckst, kann Spring committen.


27. readOnly = true

Beispiel:


@Transactional(readOnly = true)
public TaskDto findById(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));

return toDto(task);
}

Bedeutung:


Diese Methode ist nur zum Lesen von Daten gedacht.

Vorteile:


kann als Optimierungshinweis dienen
kann in manchen Fällen die Performance verbessern
kommuniziert die Absicht
hilft, versehentliche Schreiblogik zu vermeiden

Wichtig:


readOnly = true ist vor allem ein Hinweis.
Es ist nicht immer eine strikte Garantie, dass Schreiben unmöglich ist.

Merksatz:

readOnly = true bedeutet, dass diese Transaktion zum Lesen gedacht ist.


28. Read-Only-Transaktions-Falle

Schlecht:


@Transactional(readOnly = true)
public void completeTask(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
}

Problem:


Methode ändert Daten, Transaktion ist aber als read-only markiert
Verhalten kann provider-/datenbankabhängig sein
Änderungen werden möglicherweise nicht wie erwartet geflusht
das ist falsches Design

Richtig:


@Transactional
public void completeTask(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
}

Merksatz:

Ändere keine Daten innerhalb von Read-Only-Transaktionen.


29. Lese- vs. Schreibmethoden

Guter Service-Stil:


@Transactional(readOnly = true)
public TaskDto findById(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
return toDto(task);
}

@Transactional
public TaskDto create(CreateTaskRequest request) {
TaskEntity task = new TaskEntity(request.title(), "OPEN");
return toDto(taskRepository.save(task));
}

@Transactional
public void complete(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
}

Merksatz:

Lesemethoden können read-only sein. Schreibmethoden sollten read-write sein.


30. @Transactional auf Klassenebene

Du kannst @Transactional auf eine Klasse setzen.

Beispiel:


@Service
@Transactional(readOnly = true)
public class TaskService {

public TaskDto findById(Long id) {
// read-only transaction
}

@Transactional
public TaskDto create(CreateTaskRequest request) {
// overrides class-level readOnly
}
}

Bedeutung:


alle Methoden sind standardmäßig read-only
Schreibmethoden überschreiben mit @Transactional

Das ist ein häufiger Stil.

Merksatz:

@Transactional auf Klassenebene gibt Defaults; auf Methodenebene überschreibt.


31. Propagation

Propagation steuert, was passiert, wenn eine transaktionale Methode aufgerufen wird, während bereits eine Transaktion existiert.

Standard:


Propagation.REQUIRED

Bedeutung:


bestehende Transaktion beitreten, falls vorhanden
sonst neue Transaktion starten

Beispiel:


@Transactional
public void createInvoice() {
saveInvoice();
saveAuditLog();
}

Wenn saveInvoice() und saveAuditLog() auch REQUIRED nutzen, treten sie derselben Transaktion bei.

Merksatz:

Standard-Propagation ist REQUIRED.


32. Häufige Propagation-Typen

PropagationEinfache Bedeutung
REQUIREDbestehende beitreten oder neue erstellen
REQUIRES_NEWbestehende aussetzen und neue Transaktion starten
SUPPORTSbeitreten falls vorhanden, sonst ohne Transaktion laufen
MANDATORYmuss bestehende Transaktion haben
NOT_SUPPORTEDohne Transaktion laufen
NEVERfehlschlagen, wenn Transaktion existiert
NESTEDverschachtelte Transaktion mit Savepoint, falls unterstützt

Für die meisten normalen Service-Methoden:


REQUIRED reicht

33. REQUIRES_NEW

Beispiel:


@Transactional
public void createTask() {
taskRepository.save(new TaskEntity("A", "OPEN"));
auditService.writeAuditLog("TASK_CREATED");
throw new RuntimeException("main operation failed");
}

Audit-Service:


@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAuditLog(String message) {
auditLogRepository.save(new AuditLogEntity(message));
}

Bedeutung:


Audit-Log läuft in eigener Transaktion
Rollback der Haupttransaktion rollt Audit-Transaktion nicht unbedingt zurück

Vorsichtig einsetzen.

Merksatz:

REQUIRES_NEW erzeugt eine unabhängige Transaktion.


34. Isolation

Isolation steuert, wie Transaktionen die Änderungen der jeweils anderen sehen.

Häufige Isolation-Levels:


DEFAULT
READ_UNCOMMITTED
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE

Die meisten Apps nutzen:


Isolation.DEFAULT

was bedeutet:


Standard-Isolation-Level der Datenbank nutzen

Für viele Business-Apps reicht der Datenbank-Standard.

Merksatz:

Isolation steuert die Sichtbarkeit zwischen gleichzeitigen Transaktionen.


35. Timeout

Timeout definiert die maximale Transaktionsdauer.

Beispiel:


@Transactional(timeout = 5)
public void runShortOperation() {
// must finish within 5 seconds
}

Wenn die Transaktion zu lange dauert:


Transaktion kann zurückgerollt werden

Nutze es für Operationen, die nicht zu lange hängen sollen.


36. Transaktion und externe Aufrufe

Sei vorsichtig mit externen API-Aufrufen innerhalb von Transaktionen.

Schlecht:


@Transactional
public void createTask(CreateTaskRequest request) {
TaskEntity task = taskRepository.save(new TaskEntity(request.title(), "OPEN"));
externalApi.notify(task.getId());
}

Problem:


Transaktion bleibt während des Netzwerkaufrufs offen
Netzwerkaufruf kann langsam sein
Datenbank-Locks können länger gehalten werden
externes System kann erfolgreich sein, DB-Transaktion aber später zurückrollen

Bessere Optionen:


erst Datenbankänderung committen, dann Event publishen
Outbox-Pattern nutzen
asynchrone Verarbeitung nutzen
Transaction Synchronization vorsichtig nutzen

Merksatz:

Halte Transaktionen kurz und vermeide langsame externe Aufrufe darin.


37. Transaktion und Lazy Loading — Vorschau

Lazy-Beziehungen brauchen oft einen offenen Persistence Context.

Beispiel:


@Transactional(readOnly = true)
public ClientDto getClient(Long id) {
ClientEntity client = clientRepository.findById(id).orElseThrow();

// accessing lazy relationships may work inside transaction
int taskCount = client.getTasks().size();

return toDto(client, taskCount);
}

Außerhalb einer Transaktion kann Lazy Loading mit einem Lazy-Initialization-Fehler fehlschlagen.

Beziehungen und Lazy Loading schauen wir uns später an.

Merksatz:

Lazy Loading braucht normalerweise einen offenen Persistence Context.


38. Open Session in View — Warnung

Spring-Boot-Web-Apps können den Persistence Context während des Web-Request-Renderings offen halten, wenn Open Session in View aktiviert ist.

Das kann Lazy-Loading-Probleme verstecken.

Aber es kann auch führen zu:


Datenbankzugriff während View-/API-Serialisierung
N+1-Query-Problemen
weniger klaren Transaktionsgrenzen
Performance-Überraschungen

Gutes Design:


benötigte Daten in der Service-Schicht laden
innerhalb der Transaktion auf DTO mappen
DTO an den Controller zurückgeben

Merksatz:

Verlasse dich nicht auf Lazy Loading während der JSON-Serialisierung.


39. Transaktionale Tests

In Spring-Tests hat @Transactional besonderes Verhalten.

Beispiel:


@SpringBootTest
@Transactional
class TaskServiceTest {

@Test
void createsTask() {
taskService.create(new CreateTaskRequest("A"));
}
}

In vielen Spring-Test-Setups:


Transaktion wird nach dem Test zurückgerollt

Das hält die Datenbank sauber.

Merksatz:

Transaktionale Tests rollen oft standardmäßig zurück.


40. Vollständiges Beispiel: Task erstellen

Service:


@Service
public class TaskService {

private final TaskRepository taskRepository;

public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}

@Transactional
public TaskDto create(CreateTaskRequest request) {
TaskEntity task = new TaskEntity(request.title(), "OPEN");

TaskEntity saved = taskRepository.save(task);

return toDto(saved);
}

private TaskDto toDto(TaskEntity entity) {
return new TaskDto(
entity.getId(),
entity.getTitle(),
entity.getStatus()
);
}
}

Was passiert:


1. Transaktion startet.
2. Neue Entity wird erstellt.
3. save() persistiert die Entity.
4. Methode gibt DTO zurück.
5. Transaktion wird committed.
6. Insert wird dauerhaft.

41. Vollständiges Beispiel: Task mit Dirty Checking aktualisieren

Service:


@Transactional
public TaskDto complete(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));

task.complete();

return toDto(task);
}

Kein save()-Aufruf.

Was passiert:


1. Transaktion startet.
2. Entity wird geladen und wird managed.
3. Entity-Status ändert sich zu DONE.
4. Dirty Checking erkennt die Änderung.
5. SQL-Update läuft vor dem Commit.
6. Transaktion wird committed.

42. Vollständiges Beispiel: Rollback

Service:


@Transactional
public void createTwoTasksAndFail() {
taskRepository.save(new TaskEntity("Task A", "OPEN"));
taskRepository.save(new TaskEntity("Task B", "OPEN"));

throw new RuntimeException("failure");
}

Ergebnis:


Task A wird zurückgerollt.
Task B wird zurückgerollt.
Nichts wird dauerhaft gespeichert.

Weil:


RuntimeException löst standardmäßig Rollback aus.

43. Vollständiges Beispiel: Read-Only-Query

Service:


@Transactional(readOnly = true)
public Page<TaskDto> findOpenTasks(int page, int size) {
Pageable pageable = PageRequest.of(
page,
size,
Sort.by(Sort.Direction.DESC, "createdAt")
);

return taskRepository.findByStatus("OPEN", pageable)
.map(this::toDto);
}

Bedeutung:


Read-Only-Transaktion
Tasks abfragen
Entities auf DTOs mappen
keine Datenänderungen beabsichtigt

44. Häufige Prüfungsfallen

Prüfungsfalle 1

@Transactional gehört normalerweise auf Service-Methoden, nicht auf Controller.


Prüfungsfalle 2

@Transactional funktioniert über Spring-AOP-Proxies.


Prüfungsfalle 3

Self-Invocation kann den Transaktions-Proxy umgehen.


Prüfungsfalle 4

Private/interne Methodenaufrufe sind kein guter Ort für Proxy-basiertes @Transactional.


Prüfungsfalle 5

Standard-Propagation ist REQUIRED.


Prüfungsfalle 6

Standard-Rollback passiert bei RuntimeException und Error.


Prüfungsfalle 7

Checked Exceptions lösen standardmäßig kein Rollback aus.


Prüfungsfalle 8

Nutze rollbackFor für checked Exceptions, falls nötig.


Prüfungsfalle 9

Exceptions fangen und verschlucken kann ein Commit verursachen.


Prüfungsfalle 10

Dirty Checking aktualisiert managed Entities ohne explizites save().


Prüfungsfalle 11

Neue Entities brauchen save() oder persist.


Prüfungsfalle 12

readOnly = true ist vor allem ein Hinweis und sollte nicht für Schreibmethoden genutzt werden.


Prüfungsfalle 13

Flush ist nicht dasselbe wie Commit.


Prüfungsfalle 14

Halte Transaktionen kurz.


Prüfungsfalle 15

Vermeide langsame externe Aufrufe innerhalb von Transaktionen.


45. Prüfungsfrage aus der echten Prüfung: Transaktion

Frage:

Was ist eine Transaktion?

Antwort:

Eine Transaktion ist eine Arbeitseinheit, die Datenbankoperationen zusammenfasst, sodass sie entweder alle gemeinsam erfolgreich sind oder alle gemeinsam zurückgerollt werden.


46. Prüfungsfrage aus der echten Prüfung: @Transactional

Frage:

Was macht @Transactional?

Antwort:

@Transactional sagt Spring, eine Methode innerhalb einer Transaktion auszuführen. Spring startet eine Transaktion vor der Methode, committed sie bei Erfolg und rollt sie zurück, wenn eine Rollback-auslösende Exception auftritt.


47. Prüfungsfrage aus der echten Prüfung: Wo platzieren?

Frage:

Wo sollte @Transactional normalerweise platziert werden?

Antwort:

Normalerweise auf Service-Methoden, weil Services Geschäfts-Use-Cases und Transaktionsgrenzen definieren.


48. Prüfungsfrage aus der echten Prüfung: Standard-Rollback

Frage:

Welche Exceptions lösen standardmäßig Rollback aus?

Antwort:

Standardmäßig lösen RuntimeException und Error Rollback aus. Checked Exceptions lösen standardmäßig kein Rollback aus.


49. Prüfungsfrage aus der echten Prüfung: rollbackFor

Frage:

Wie kann ich eine checked Exception zum Rollback bringen?

Antwort:

Nutze rollbackFor.


@Transactional(rollbackFor = IOException.class)

50. Prüfungsfrage aus der echten Prüfung: Persistence Context

Frage:

Was ist der Persistence Context?

Antwort:

Der Persistence Context ist ein First-Level-Cache und Tracking-Bereich für managed JPA-Entities. Er trackt Entity-Änderungen während einer Transaktion.


51. Prüfungsfrage aus der echten Prüfung: Dirty Checking

Frage:

Was ist Dirty Checking?

Antwort:

Dirty Checking ist der JPA-Mechanismus, der Änderungen an managed Entities automatisch erkennt und diese Änderungen während Flush oder Commit mit der Datenbank synchronisiert.


52. Prüfungsfrage aus der echten Prüfung: readOnly

Frage:

Was bedeutet @Transactional(readOnly = true)?

Antwort:

Es bedeutet, dass die Transaktion für Read-Only-Arbeit gedacht ist. Es kann als Optimierungshinweis genutzt werden und kommuniziert die Absicht, sollte aber nicht für Methoden genutzt werden, die Daten ändern.


53. Prüfungsfrage aus der echten Prüfung: Self-Invocation

Frage:

Warum kann Self-Invocation @Transactional brechen?

Antwort:

Weil @Transactional normalerweise über einen Spring-Proxy angewendet wird. Wenn eine Methode innerhalb derselben Klasse eine andere Methode derselben Klasse aufruft, geht der Aufruf nicht durch den Proxy — das transaktionale Advice wird möglicherweise nicht angewendet.


54. Prüfungsfrage aus der echten Prüfung: Flush vs. Commit

Frage:

Was ist der Unterschied zwischen Flush und Commit?

Antwort:

Flush synchronisiert Änderungen im Persistence Context mit der Datenbank, indem SQL gesendet wird. Commit macht die Transaktionsänderungen dauerhaft.


55. Interview-Antwort

Frage:

Erkläre @Transactional.

Gute Antwort:

@Transactional definiert eine Transaktionsgrenze um eine Methode. Spring nutzt AOP-Proxies, um vor der Methode eine Transaktion zu starten, sie bei erfolgreichem Abschluss zu committen und bei einer Rollback-auslösenden Exception zurückzurollen. Es wird normalerweise auf Service-Methoden gesetzt, weil eine Service-Methode eine geschäftliche Arbeitseinheit darstellt.


56. Interview-Antwort

Frage:

Was ist Dirty Checking?

Gute Antwort:

Dirty Checking ist ein JPA-Feature, bei dem Änderungen an managed Entities automatisch erkannt werden. Wenn eine Entity innerhalb einer Transaktion geladen wird und du eines ihrer Felder änderst, trackt JPA diese Änderung im Persistence Context und sendet beim Flush oder Commit ein Update-SQL. Das bedeutet, du musst nach dem Ändern einer bestehenden managed Entity oft kein save() aufrufen.


57. Interview-Antwort

Frage:

Was löst Rollback in Spring-Transaktionen aus?

Gute Antwort:

Standardmäßig rollt Spring Transaktionen bei unchecked Exceptions zurück — also RuntimeException — und auch bei Error. Checked Exceptions lösen standardmäßig kein Rollback aus. Wenn du Rollback für eine checked Exception brauchst, kannst du rollbackFor konfigurieren, zum Beispiel @Transactional(rollbackFor = IOException.class).


58. Interview-Antwort

Frage:

Warum sollten Transaktionen normalerweise in der Service-Schicht sein?

Gute Antwort:

Die Service-Schicht repräsentiert Geschäfts-Use-Cases. Eine einzelne Service-Methode kann mehrere Repositories aufrufen, mehrere Entities aktualisieren und Audit-Logs schreiben. Diese Operationen sollten normalerweise gemeinsam committen oder zurückrollen. Controller sollten sich auf HTTP konzentrieren, und Repositories auf Persistenzzugriff — deshalb ist die Service-Schicht der beste Ort für Transaktionsgrenzen.


59. Interview-Antwort

Frage:

Was ist das Self-Invocation-Problem mit @Transactional?

Gute Antwort:

Spring wendet @Transactional normalerweise über einen Proxy an. Wenn eine Methode in derselben Klasse eine andere transaktionale Methode direkt aufruft, geht der Aufruf nicht durch den Proxy. Deshalb läuft das transaktionale Advice möglicherweise nicht. Eine häufige Lösung ist, @Transactional auf die äußere öffentliche Service-Methode zu setzen oder die transaktionale Methode in einen anderen Spring Bean zu verschieben.


60. Kleine Code-Übung

Service:


@Service
public class TaskService {

private final TaskRepository taskRepository;

public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}

@Transactional
public TaskDto complete(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));

task.complete();

return toDto(task);
}
}

Fragen:

  1. Wo startet die Transaktion?
  2. Ist task managed?
  3. Warum ist kein save() nötig?
  4. Wann wird die Datenbank aktualisiert?
  5. Was passiert, wenn ResourceNotFoundException RuntimeException erweitert?

Antworten:

  1. Vor dem Aufruf von complete().
  2. Ja, innerhalb der Transaktion.
  3. Dirty Checking trackt die Änderung an der managed Entity.
  4. Während Flush oder Commit.
  5. Die Transaktion wird standardmäßig zurückgerollt.

61. Kleine Bug-Übung 1

Problem:


@Transactional(readOnly = true)
public void complete(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
}

Frage:

Was ist falsch?

Antwort:

Die Methode ändert Daten, ist aber als read-only markiert. Nutze normales @Transactional für Schreibmethoden.


62. Kleine Bug-Übung 2

Problem:


@Transactional
public void importTasks() throws IOException {
taskRepository.save(new TaskEntity("A", "OPEN"));
throw new IOException("File error");
}

Frage:

Was ist die Falle?

Antwort:

IOException ist eine checked Exception. Checked Exceptions lösen standardmäßig kein Rollback aus. Nutze:


@Transactional(rollbackFor = IOException.class)

wenn Rollback nötig ist.


63. Kleine Bug-Übung 3

Problem:


public void outer() {
inner();
}

@Transactional
public void inner() {
taskRepository.save(new TaskEntity("A", "OPEN"));
}

Frage:

Was ist falsch?

Antwort:

Wenn outer() und inner() in derselben Klasse sind, geht der interne Aufruf nicht durch den Spring-Proxy. Das @Transactional auf inner() wird möglicherweise nicht angewendet.


Übungsfragen

Frage 1

Was ist eine Transaktion?

Antwort:

Eine Transaktion ist eine Arbeitseinheit, die Datenbankoperationen zusammenfasst, sodass sie entweder alle gemeinsam erfolgreich sind oder alle gemeinsam zurückgerollt werden.


Frage 2

Warum brauchen wir Transaktionen?

Antwort:

Transaktionen verhindern halbfertige Datenbankänderungen und schützen die Datenkonsistenz.


Frage 3

Was bedeutet ACID?

Antwort:

ACID bedeutet Atomicity, Consistency, Isolation und Durability.


Frage 4

Was macht @Transactional?

Antwort:

@Transactional sagt Spring, eine Methode innerhalb einer Transaktion auszuführen — bei Erfolg committen, bei Rollback-auslösenden Exceptions zurückrollen.


Frage 5

Wo sollte @Transactional normalerweise platziert werden?

Antwort:

Normalerweise auf öffentliche Service-Methoden, weil Services Geschäfts-Use-Cases und Transaktionsgrenzen definieren.


Frage 6

Warum funktioniert @Transactional über Proxies?

Antwort:

Spring nutzt AOP-Proxies, um Transaktionsverhalten um Methodenaufrufe herum anzuwenden.


Frage 7

Was ist die Self-Invocation-Falle?

Antwort:

Self-Invocation passiert, wenn eine Methode in einer Klasse eine andere Methode in derselben Klasse aufruft. Der Aufruf umgeht den Spring-Proxy, sodass @Transactional möglicherweise nicht angewendet wird.


Frage 8

Was ist der Persistence Context?

Antwort:

Der Persistence Context ist ein First-Level-Cache und Tracking-Bereich für managed JPA-Entities.


Frage 9

Was ist eine managed Entity?

Antwort:

Eine managed Entity ist eine Entity, die aktuell vom Persistence Context getrackt wird.


Frage 10

Was ist Dirty Checking?

Antwort:

Dirty Checking ist die automatische Erkennung von Änderungen an managed Entities durch JPA. Änderungen werden während Flush oder Commit mit der Datenbank synchronisiert.


Frage 11

Brauche ich save() nach dem Ändern einer managed Entity?

Antwort:

Normalerweise nein — nicht für eine bestehende managed Entity innerhalb einer Transaktion. Dirty Checking kann die Änderungen automatisch speichern.


Frage 12

Was ist Flush?

Antwort:

Flush synchronisiert Änderungen im Persistence Context mit der Datenbank, indem SQL-Statements gesendet werden.


Frage 13

Was ist der Unterschied zwischen Flush und Commit?

Antwort:

Flush sendet SQL an die Datenbank. Commit macht die Transaktionsänderungen dauerhaft.


Frage 14

Welche Exceptions lösen standardmäßig Rollback aus?

Antwort:

RuntimeException und Error lösen standardmäßig Rollback aus.


Frage 15

Lösen checked Exceptions standardmäßig Rollback aus?

Antwort:

Nein. Checked Exceptions lösen standardmäßig kein Rollback aus.


Frage 16

Wie kann ich für eine checked Exception zurückrollen?

Antwort:

Nutze rollbackFor.


@Transactional(rollbackFor = IOException.class)

Frage 17

Was passiert, wenn ich eine Runtime Exception innerhalb einer transaktionalen Methode fange und verschlucke?

Antwort:

Wenn die Exception gefangen und nicht erneut geworfen wird, kann die Methode normal zurückkehren und Spring kann die Transaktion committen.


Frage 18

Was bedeutet readOnly = true?

Antwort:

Es bedeutet, dass die Transaktion für Read-Only-Arbeit gedacht ist. Es kann ein Optimierungshinweis sein und kommuniziert die Absicht, sollte aber nicht für Schreiboperationen genutzt werden.


Frage 19

Was ist die Standard-Transaction-Propagation?

Antwort:

Die Standard-Propagation ist REQUIRED.


Frage 20

Warum sollten Transaktionen kurz gehalten werden?

Antwort:

Lange Transaktionen können Datenbankressourcen und Locks halten, die Performance reduzieren und Probleme verursachen, wenn sie langsame externe Aufrufe enthalten.

Merksätze zum Mitnehmen

  • Eine Transaktion ist eine Alles-oder-nichts-Arbeitseinheit.
  • @Transactional definiert eine Transaktionsgrenze.
  • Setze Transaktionen um Geschäfts-Use-Cases — normalerweise in Services.
  • Spring wendet @Transactional über AOP-Proxies an.
  • Self-Invocation kann @Transactional umgehen.
  • Standard-Propagation ist REQUIRED.
  • Der Persistence Context trackt managed Entities.
  • Dirty Checking aktualisiert managed Entities automatisch.
  • Neue Entities brauchen save().
  • Bestehende managed Entities können ohne save() aktualisiert werden.
  • Flush sendet SQL.
  • Commit macht Änderungen dauerhaft.
  • Runtime Exceptions rollen standardmäßig zurück.
  • Checked Exceptions rollen standardmäßig nicht zurück.
  • Nutze rollbackFor für checked Exceptions.
  • Exceptions fangen und verschlucken kann ein Commit verursachen.
  • readOnly = true ist für Lesemethoden.
  • Schreibe keine Daten in Read-Only-Transaktionen.
  • Halte Transaktionen kurz.
  • Vermeide langsame externe Aufrufe innerhalb von Transaktionen.