JPA und Transaktionen
Transaktionsgrenzen, Lazy Loading und Optimistic Locking an einem Order-Slice üben.
Workshop-Slice
Dieses Lab zeigt einen konkreten Enterprise-Fehler mit Vorher/Nachher-Code. Die Beispiele sind bewusst fachlich benannt, damit du Architekturentscheidungen und nicht nur Syntax übst.
Lernziel
Du setzt Transaktionsgrenzen bewusst in der Application-Schicht und verhinderst typische JPA-Fallen.
Ausgangslage
Ein Service lädt Entities im Controller, ändert sie außerhalb klarer Transaktionsgrenzen und erzeugt LazyInitializationException oder verlorene Updates.
Vorher: problematischer Code
Die Entity wird bis in den Web-Adapter gereicht. Die Transaktion ist implizit und die fachliche Änderung kann parallel überschrieben werden.
@GetMapping("/orders/{id}")
OrderEntity details(@PathVariable UUID id) {
OrderEntity entity = entityManager.find(OrderEntity.class, id);
// Lazy Collection wird später in JSON serialisiert -> Fehler außerhalb der Transaktion.
return entity;
}
@PostMapping("/orders/{id}/confirm")
void confirm(@PathVariable UUID id) {
OrderEntity entity = entityManager.find(OrderEntity.class, id);
entity.setStatus("CONFIRMED");
entityManager.merge(entity);
}
Analyse: Was ist daran schlecht?
- JPA-Entity verlässt die Persistenzgrenze und wird API-Vertrag.
- Lazy Collections können außerhalb der Transaktion brechen.
- Keine Optimistic-Lock-Prüfung bei parallelen Updates.
- Controller kennt EntityManager und Transaktionsdetails.
Nachher: bessere Lösung
Die Zielversion nutzt einen transaktionalen Use Case, ein Repository und eine versionierte Entity.
@Service
class ConfirmOrderService {
private final OrderJpaRepository repository;
@Transactional
public void confirm(OrderId id, long expectedVersion) {
OrderEntity entity = repository.findByBusinessId(id.value())
.orElseThrow(() -> new OrderNotFound(id));
if (entity.getVersion() != expectedVersion) {
throw new ConcurrentOrderModification(id);
}
entity.confirm();
}
}
@Entity
@Table(name = "orders")
class OrderEntity {
@Id
private UUID id;
@Version
private long version;
@Column(nullable = false, unique = true)
private String businessId;
@Enumerated(EnumType.STRING)
private OrderStatus status;
void confirm() {
if (status == OrderStatus.CONFIRMED) {
throw new OrderAlreadyConfirmed(businessId);
}
status = OrderStatus.CONFIRMED;
}
}
Test / Prüfnachweis
Dieser Abschnitt zeigt, wie du die Verbesserung nachweist. Es ist bewusst kein reiner Happy-Path-Test, sondern prüft ein Risiko aus dem Vorher-Teil.
@Test
void rejectsStaleVersion() {
var id = new OrderId("O-44");
service.confirm(id, 0L);
assertThrows(ConcurrentOrderModification.class,
() -> service.confirm(id, 0L));
}
Typische Fehler
- @Transactional am Controller statt am Use Case.
- Entity direkt als JSON ausgeben.
- FetchType.EAGER als pauschale Lösung für Lazy-Probleme.
- Version-Feld weglassen, obwohl parallele Bearbeitung möglich ist.
Deep-Learning-Bezug
Die Links führen zum ausführlichen Inhalt; die Deep-Learning-Seite bleibt nur die Lernlandkarte und kopiert den Inhalt nicht doppelt.
Prüfcheckliste
- Transaktion liegt im Application Service.
- Entity verlässt nicht die REST-Grenze.
- Optimistic Locking oder Versionsprüfung ist sichtbar.
- Fehlerfälle sind testbar.