Lab 04

JPA und Transaktionen

Transaktionsgrenzen, Lazy Loading und Optimistic Locking an einem Order-Slice üben.

JPATransaktionRepositoryOptimistic Lock
SchwierigkeitMittel
Dauer60–90 Min
LernstufeStufe 6 – Praxislabor und Capstone
Arbeitsweise: Erst den Vorher-Code öffnen, dann Analyse lesen, anschließend die Nachher-Lösung und den Test vergleichen. Alle Abschnitte sind standardmäßig geschlossen.

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.

OrderJpaController.javaJAVA
@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.

ConfirmOrderService.javaJAVA
@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();
    }
}
OrderEntity.javaJAVA
@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.

OrderJpaConcurrencyTest.javaJAVA
@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.
⌂ Cockpit