JPA, Hibernate & Persistenz
Entities, Aggregate, Fetching, Locking, Migrationen und Performancefallen.
Entity ist nicht automatisch Domain
JPA/Hibernate erleichtert Persistenz, kann aber Fachmodell und Datenmodell vermischen. Eine Entity ist ein persistierbares Objekt mit Lifecycle, Identität und Mapping-Regeln. Ein Domain-Aggregat ist ein fachlicher Konsistenzbereich. Beide können identisch sein, müssen es aber nicht.
package com.example.order.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
// Pattern: Aggregate Root - Order schützt die Konsistenz der Positionsliste und Statusübergänge.
public class Order {
private final OrderId id;
private final CustomerId customerId;
private final List<OrderLine> lines = new ArrayList<>();
private OrderStatus status = OrderStatus.DRAFT;
public Order(OrderId id, CustomerId customerId) {
this.id = Objects.requireNonNull(id);
this.customerId = Objects.requireNonNull(customerId);
}
public static Order draftFor(CustomerId customerId) {
return new Order(new OrderId(UUID.randomUUID()), customerId);
}
public void addLine(String sku, int quantity, BigDecimal unitPrice) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Only draft orders can be changed");
}
if (quantity <= 0 || unitPrice.signum() < 0) {
throw new IllegalArgumentException("Quantity and price must be valid");
}
lines.add(new OrderLine(sku, quantity, unitPrice));
}
public void place() {
if (lines.isEmpty()) throw new IllegalStateException("Order must contain at least one line");
status = OrderStatus.PLACED;
}
public BigDecimal total() {
return lines.stream()
.map(OrderLine::lineTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
N+1 und Fetching verstehen
Viele Performanceprobleme entstehen nicht durch „Hibernate ist langsam“, sondern durch unpassende Fetch-Strategien für konkrete Use Cases. Schreibmodelle brauchen Konsistenz. Lesemodelle brauchen oft Projektionen.
// Problem: N+1 Queries, wenn jede Order später lazy ihre Lines lädt.
List<OrderEntity> orders = entityManager
.createQuery("select o from OrderEntity o", OrderEntity.class)
.getResultList();
orders.forEach(o -> log.info("{} lines", o.getLines().size()));
// Besser für Lese-Use-Case: gezielte Fetch-Strategie oder Projektion.
List<OrderSummary> summaries = entityManager
.createQuery("""
select new com.example.OrderSummary(o.id, c.name, sum(l.quantity * l.unitPrice))
from OrderEntity o
join o.customer c
join o.lines l
group by o.id, c.name
""", OrderSummary.class)
.getResultList();
Locking und Konsistenz
| Mechanismus | Wann nutzen | Risiko |
|---|---|---|
| Optimistic Locking | Viele parallele Lese-/Schreibzugriffe mit seltenen Konflikten | Konflikt muss fachlich sauber behandelt werden |
| Pessimistic Locking | Kurze kritische Abschnitte mit hohem Konfliktrisiko | Deadlocks, lange Transaktionen, Skalierungsprobleme |
| Unique Constraints | Eindeutige fachliche Regeln | Fehler müssen auf Fachfehler gemappt werden |
| Outbox Tabelle | DB-Änderung plus Event-Veröffentlichung | Zusätzlicher Prozess für Versand und Cleanup nötig |
Migrationen
Schema-Migrationen gehören in den Delivery-Prozess. Tools wie Flyway oder Liquibase sind nicht nur technische Helfer, sondern ein Vertrag zwischen Code-Version und Datenbankschema. Jede Migration sollte klein, nachvollziehbar, vorwärtskompatibel und rollback-bewusst sein.
-- V20260707_01__create_order_tables.sql
create table orders (
id uuid primary key,
customer_id uuid not null,
status varchar(30) not null,
version bigint not null default 0,
created_at timestamp not null
);
create table order_lines (
id uuid primary key,
order_id uuid not null references orders(id),
sku varchar(80) not null,
quantity integer not null check (quantity > 0),
unit_price numeric(19,2) not null check (unit_price >= 0)
);