JPA, Hibernate & Persistenz

Entities, Aggregate, Fetching, Locking, Migrationen und Performancefallen.

Enterprise JavaBeispieleArchitekturOffline HTML

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.

Domain-Aggregat ohne Framework-Abhängigkeit
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.

N+1 Problem und Projektion
// 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();
Praxisregel: Für API-Listen selten komplette Aggregate laden. Häufig ist eine projektionierte Query stabiler, schneller und klarer.

Locking und Konsistenz

MechanismusWann nutzenRisiko
Optimistic LockingViele parallele Lese-/Schreibzugriffe mit seltenen KonfliktenKonflikt muss fachlich sauber behandelt werden
Pessimistic LockingKurze kritische Abschnitte mit hohem KonfliktrisikoDeadlocks, lange Transaktionen, Skalierungsprobleme
Unique ConstraintsEindeutige fachliche RegelnFehler müssen auf Fachfehler gemappt werden
Outbox TabelleDB-Änderung plus Event-VeröffentlichungZusätzlicher Prozess für Versand und Cleanup nötig
Persistenz und Transaktion gehören bewusst an den Use Case angebunden.
Persistenz und Transaktion gehören bewusst an den Use Case angebunden.

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.

Kleine, fachlich benannte Datenbankmigration
-- V30260707_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)
);
⌂ Cockpit