Hibernate 7 Migration: So überführst du deine Anwendung ohne Drama

Julian | May 8, 2026 min read

Nachdem wir in dieser Serie die Architektur verstanden, Performance-Killer eliminiert und gefährliche Config-Defaults gebändigt haben, kommen wir heute zur vielleicht wichtigsten Frage: Wie komme ich eigentlich dorthin?

Eine bestehende Anwendung auf Hibernate 7 zu migrieren ist kein Hexenwerk – aber es ist auch kein Zwei-Zeilen-Diff. Die schlechte Nachricht: Es gibt Breaking Changes. Die gute Nachricht: Die meisten davon folgen einem klaren Muster, und wer die Vorgängerartikel dieser Serie gelesen hat, kennt die Denkweise dahinter bereits.

In diesem Artikel gehen wir systematisch vor. Kein blindes Upgrade-Hoffen, kein „mal schauen was der Compiler sagt". Stattdessen: ein strukturierter Weg von der alten Codebasis zur neuen – mit allen Fallstricken, die dir auf dem Weg begegnen werden.

Was hat sich in Hibernate 7 eigentlich verändert?

Bevor du auch nur eine Dependency anfasst, lohnt ein kurzer Blick auf das große Bild. Hibernate 7 ist kein inkrementelles Update – es ist eine Generationengrenze. Drei Dinge haben sich fundamental verändert:

Java 17 ist jetzt Pflicht. Kein Opt-in, kein Workaround. Wenn du noch auf Java 11 bist, ist das dein erster Schritt – noch vor Hibernate.

Jakarta Persistence 3.2. Das ist der größte Eingriff ins Typsystem. Die Criteria API, die Entity Graph API, viele zentrale Interfaces – sie alle haben neue Typparameter bekommen oder wurden umgebaut, weil JPA 3.2 Dinge standardisiert hat, die Hibernate bisher in eigenen Erweiterungen gelöst hat. Das bedeutet: Bytecode-Inkompatibilitäten, also Sachen, die compilieren aber zur Laufzeit knallen.

Aufräumen auf ganzer Linie. Hibernate 7 hat konsequent entfernt, was seit Jahren als deprecated markiert war. Session#save, Session#update, Session#delete, diverse Annotations wie @Where, @Proxy, @LazyCollection – weg. Wer diese „noch schnell mal nutzen" wollte: Die Zeit ist abgelaufen.

Das klingt viel. Ist es auch. Aber der Großteil der Änderungen folgt einem Muster: Hibernate-eigene API → JPA-Standard. Wenn du weißt, was fliegt, weißt du sofort, womit du es ersetzt.

💡 Der offizielle Hibernate 7 Migration Guide ist dein bester Freund bei diesem Upgrade. Lesezeichen setzen, Tab offen lassen.

Schritt 1: Dependency-Update und erster Compile

Fangen wir mit dem Offensichtlichen an. In deiner pom.xml tauschst du die Hibernate-Version:

<!-- Vorher -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.6.x.Final</version>
</dependency>

<!-- Nachher -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>7.0.x.Final</version>
</dependency>

Gradle-Nutzer tauschen entsprechend die Version in ihrer build.gradle. Wer das Bytecode-Enhancement-Plugin nutzt, hat außerdem eine böse Überraschung: Das Maven-Plugin hat in Hibernate 7 eine neue groupId und artifactId bekommen:

<!-- Vorher -->
<plugin>
    <groupId>org.hibernate.orm.tooling</groupId>
    <artifactId>hibernate-enhance-maven-plugin</artifactId>
    ...
</plugin>

<!-- Nachher -->
<plugin>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-maven-plugin</artifactId>
    <version>7.0.x.Final</version>
    <executions>
        <execution>
            <configuration>
                <enableLazyInitialization>true</enableLazyInitialization>
                <enableDirtyTracking>true</enableDirtyTracking>
            </configuration>
            <goals>
                <goal>enhance</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Jetzt: Build starten und die Fehlerliste lesen. Nicht erschrecken – das ist kein Zeichen, dass etwas falsch läuft. Es ist dein persönlicher Migration-Backlog, automatisch generiert vom Compiler.

Sortiere die Fehler grob in zwei Kategorien:

  • Removed APIs – Methoden und Klassen, die nicht mehr existieren (Session#save, @Where, usw.)
  • Typkonflikte – Signaturen, die sich durch Jakarta Persistence 3.2 verändert haben

Arbeite die Removed APIs zuerst ab. Die sind in der Regel mechanical fixes – ein Methodenname gegen einen anderen austauschen. Die Typkonflikte brauchen manchmal etwas mehr Nachdenken.

⚠️ Achtung, Connection Pools: Hast du Vibur, Proxool oder UCP als Connection Pool in der Dependency? Die werden in Hibernate 7 nicht mehr mitgeliefert. Wechsle auf HikariCP oder Agroal – beide sind produktionserprobt und besser getestet.

Schritt 2: Deprecated APIs auflösen

Das sind die häufigsten Compile-Fehler, auf die du stoßen wirst – und ihre direkten Lösungen.

Session-Methoden: Die alten Klassiker sind weg

Das ist der größte Batzen. Hibernate hat konsequent auf JPA-Standard umgestellt:

// VORHER
session.save(entity);
session.update(entity);
session.saveOrUpdate(entity);
session.delete(entity);
entity = session.load(Product.class, id);

// NACHHER
session.persist(entity);           // war transient → wird managed
session.merge(entity);             // war detached → wieder managed
session.persist(entity);           // transient → persist
session.merge(entity);             // detached → merge
session.remove(entity);
entity = session.getReference(Product.class, id);

Die Semantik von persist vs. merge ist dabei nicht identisch mit dem alten save/update. Wenn du dir unsicher bist, ob deine Entity transient oder detached ist: Im Zweifel merge – es handhabt beide Fälle korrekt.

Annotations: Der große Frühjahrsputz

Hibernate hat eine Reihe eigener Annotations zugunsten von JPA-Standards oder neuerer Alternativen entfernt:

// VORHER
@Where(clause = "deleted = false")
@OneToMany
private List<Order> orders;

@OrderBy("name ASC")  // Hibernate-eigene @OrderBy
private List<Tag> tags;

@LazyCollection(LazyCollectionOption.EXTRA)
private Set<Child> children;

// NACHHER
@SQLRestriction("deleted = false")
@OneToMany
private List<Order> orders;

@SQLOrder("name ASC")         // oder JPA @OrderBy
private List<Tag> tags;

// @LazyCollection ersatzlos entfernt – Hibernate entscheidet selbst
private Set<Child> children;

Und noch zwei häufige Fallen:

// VORHER
@Proxy(lazy = false)
@Entity
public class Product { ... }

@Target(ProductImpl.class)
@Embedded
private Product product;

// NACHHER
// @Proxy entfernt → FetchType.EAGER oder @ConcreteProxy verwenden
@Entity
public class Product { ... }

@TargetEmbeddable(ProductImpl.class)
@Embedded
private Product product;

LockOptions: Schlanker durch direktes API

// VORHER
Book book = session.find(
    Book.class,
    1,
    new LockOptions(LockMode.PESSIMISTIC_WRITE, 0)
);

// NACHHER
Book book = session.find(
    Book.class,
    1,
    LockMode.PESSIMISTIC_WRITE,
    Timeouts.NO_WAIT
);

💡 Tipp für große Codebases: Suche erst global nach den Methodennamen (session.save(, @Where(, @LazyCollection) bevor du anfängst zu ersetzen. So bekommst du eine realistische Schätzung des Aufwands – und kannst Prioritäten setzen.

Schritt 3: Die Config-Falle

Wer den Config-Artikel dieser Serie gelesen hat, weiß: Hibernate-Defaults sind nicht immer dein Freund. Bei der Migration gilt das genauso – nur dass diesmal einige Properties, auf die du dich verlassen hast, entweder verschwunden sind oder ein neues Standardverhalten mitbringen.

Properties, die nicht mehr existieren

# VORHER – hat Hibernate erlaubt, detached Entities zu refreshen
hibernate.allow_refresh_detached_entity=true

# NACHHER – gibt es nicht mehr. Hibernate wirft jetzt immer eine
# IllegalArgumentException wenn du eine detached Entity refresht.
# Fix: Entity vorher mit merge() re-attachen.

Properties mit neuem Standardverhalten

Das ist die gefährlichere Kategorie – kein Compile-Fehler, kein Laufzeitfehler beim Start. Es passiert einfach etwas anderes als vorher:

# Immutable Entities: Vorher gab es nur eine Warning, jetzt eine Exception
# Wenn du UPDATE-Queries auf @Immutable-Entities fährst, bricht es jetzt.
# Temporärer Workaround während der Migration:
hibernate.query.immutable_entity_update_query_handling_mode=allow

# Native Queries: Vorher kamen java.sql-Typen (Date, Timestamp) zurück,
# jetzt kommen java.time-Typen (LocalDate, LocalDateTime).
# Wenn du noch auf java.sql-Typen castest:
hibernate.query.native.prefer_jdbc_datetime_types=true

# XML-Spalten (PostgreSQL, Oracle): Format hat sich geändert.
# Bestehende Daten müssen migriert oder Kompatibilitätsmodus aktiviert werden:
hibernate.type.xml_format_mapper.legacy_format=true

# Array-Mapping (MySQL/MariaDB): War VARBINARY, ist jetzt JSON
# Bestehende Daten müssen migriert werden, oder:
hibernate.type.preferred_array_jdbc_type=VARBINARY

⚠️ Diese Properties sind Notfallbremsen, keine Dauerlösung. Nutze sie um die Migration nicht zu blockieren – aber plane, sie wieder rauszunehmen.

StatelessSession: Zwei stille Verhaltensänderungen

Wenn du StatelessSession für Batch-Processing nutzt, aufgepasst:

// NEU: StatelessSession nutzt jetzt den Second-Level Cache.
// Wenn du das NICHT willst (z.B. bei Bulk-Imports):
statelessSession.setCacheMode(CacheMode.IGNORE);

// NEU: hibernate.jdbc.batch_size hat keinen Effekt mehr auf StatelessSession.
// Stattdessen: explizites Batching nutzen
statelessSession.insertMultiple(entities);
statelessSession.updateMultiple(entities);
statelessSession.deleteMultiple(entities);

Entity Discovery: Neues Modul nötig

Wenn deine Anwendung auf automatischem Classpath-Scanning für Entities basiert (also ohne explizite Auflistung in persistence.xml), brauchst du ein zusätzliches Modul:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-scan-jandex</artifactId>
    <version>7.0.x.Final</version>
</dependency>

Ohne dieses Modul werden deine Entities schlicht nicht gefunden. Auch das ist ein stiller Fehler – kein Compile-Problem, aber zur Laufzeit fehlen plötzlich Tabellen-Mappings.

Schritt 4: Tests als Sicherheitsnetz

Du hast die APIs umgestellt, die Config bereinigt – aber woher weißt du, dass deine Anwendung noch das tut, was sie soll? Genau hier trennt sich strukturierte Migration von blindem Optimismus.

Was du testen musst

Hibernate 7 ändert nicht nur Methoden-Namen, es ändert auch Verhalten. Das sind die drei Bereiche, die deine Tests abdecken müssen:

1. Entity-Mapping und Validierungen

Hibernate 7 ist erheblich strenger bei falschen Annotation-Kombinationen. Fehler, die vorher still ignoriert wurden, werfen jetzt Exceptions beim Start. Dein Test-Kontextstart ist dein erster Indikator:

@Test
void applicationContextLoads() {
    // Wenn dieser Test fehlschlägt, hat Hibernate ein Mapping-Problem entdeckt.
    // Fehlermeldung lesen – sie ist jetzt deutlich präziser als früher.
}

Typische Fehler, die hier auftauchen:

  • Mehrere PersistentAttributeType-Annotations auf einem Feld (@Basic + @ManyToOne)
  • Annotations am falschen Ort (Getter statt Feld bei Field-Access)
  • AttributeConverter auf inkompatiblen Feldern (@Id, @Version, @Enumerated)

2. Queries – besonders Native Queries

Das neue Default-Verhalten bei Native Queries (Rückgabe von java.time statt java.sql) macht sich im Test sofort bemerkbar:

// VORHER – hat funktioniert
List<Object[]> results = session.createNativeQuery(
    "SELECT created_at FROM orders WHERE id = :id"
).setParameter("id", 1L).getResultList();
Timestamp createdAt = (Timestamp) results.get(0)[0]; // java.sql.Timestamp

// NACHHER – ClassCastException zur Laufzeit
// Fix: Expliziter Cast auf java.time oder Property aktivieren
LocalDateTime createdAt = (LocalDateTime) results.get(0)[0]; // java.time

3. Cascade-Verhalten

Die Änderungen rund um CascadeType.SAVE_UPDATE und detached Entities sind die tückischsten – sie zeigen sich nicht beim Compile, nicht beim Start, sondern erst beim Flush:

@Test
void addingDetachedChildShouldRequireMergeFirst() {
    Child detachedChild = getDetachedChild();
    Parent parent = session.find(Parent.class, parentId);

    // VORHER: hat funktioniert
    // NACHHER: EntityExistsException beim flush()
    // parent.addChild(detachedChild); ← das bricht jetzt

    // Korrekt:
    Child managedChild = session.merge(detachedChild);
    parent.addChild(managedChild);
    session.flush(); // muss durch
}

In-Memory DB oder Testcontainers?

Kurze Antwort: Beides, mit klarer Aufgabenteilung.

H2 / In-MemoryTestcontainers
WofürMapping, Basic Queries, Cascade-LogikDDL-Changes, DB-spezifisches Verhalten
GeschwindigkeitSehr schnellLangsamer (Container-Start)
Wann nötigImmerBei Oracle/MySQL-spezifischen Änderungen (Float-DDL, Array-Mapping)

Gerade die DDL-Änderungen – etwa das neue binary_float/binary_double auf Oracle oder das geänderte Array-Mapping auf MySQL – kannst du mit H2 nicht testen. Da brauchst du den echten Datenbanktyp.

💡 Strategie: Lass deine bestehende Testsuite zuerst durchlaufen, bevor du anfängst Code zu ändern. Die Fehler, die du siehst, sind deine Migrationsliste. Dann arbeite sie ab und sieh zu, wie die roten Balken grün werden.

Die häufigsten Fallstricke

⚠️ @Id und @MapsId cascaden nicht mehr automatisch

Früher hat Hibernate für Felder mit @Id oder @MapsId automatisch cascade=PERSIST aktiviert. Jetzt nicht mehr. Wenn deine Entities darauf angewiesen waren, musst du es explizit hinzufügen – sonst verschwinden Daten beim Persist lautlos.

@MapsId
@ManyToOne(cascade = CascadeType.PERSIST) // jetzt explizit nötig
private User user;

⚠️ Criteria API: Implicit Treats sind Geschichte

Subtyp-Attribute über die Criteria API direkt anzusprechen war früher still erlaubt. Jetzt wirft Hibernate eine Exception. Du musst explizit downcasten:

// VORHER – hat funktioniert, war aber spec-widrig
root.get("subTypeAttribute");

// NACHHER
cb.treat(root, SubType.class).get("subTypeAttribute");

⚠️ isXxx()-Getter als Property-Namen verboten

Das JavaBean-Pattern isDefault() als Property-Deklaration war früher fragwürdig aber erlaubt. Hibernate 7 verweigert das jetzt hart. Rename auf getDefault() oder mappe das Feld direkt.


⚠️ GenerationType.SEQUENCE ohne @SequenceGenerator – jetzt ein Fehler

Früher war Hibernate bei der Kombination @GeneratedValue(strategy = GenerationType.SEQUENCE) ohne passenden @SequenceGenerator großzügig. Jetzt nicht mehr. Entweder explizit annotieren oder auf GenerationType.AUTO wechseln.


⚠️ ValidationMode.AUTO schmeißt jetzt durch

Wenn die ValidatorFactory-Erstellung eine Exception wirft, wurde die früher geschluckt. Jetzt propagiert Hibernate sie. Falls deine Bean Validation-Konfiguration leise kaputt war, weißt du es spätestens jetzt.


⚠️ @OrderColumn in unowned @OneToMany – weniger SQL, anderes Verhalten

Hibernate hat früher superfluous UPDATE-Statements generiert um die Order-Column zu setzen. Das passiert jetzt nur noch wenn die Column nicht auch als Feld gemappt ist. Wer darauf implizit angewiesen war: Prüfen, ob die Reihenfolge noch stimmt.

Fazit

Wir sind auf der Zielgeraden angekommen.

In dieser Serie haben wir Hibernate 7 von allen Seiten beleuchtet: die Architektur, die Performance-Fallen, die gefährlichen Config-Defaults – und heute den Weg dorthin. Migration ist kein Drama, wenn du weißt, was dich erwartet. Und jetzt weißt du es.

Die wichtigsten Takeaways für deinen Upgrade-Weg:

  • Zuerst lesen, dann anfassen. Der offizielle Migration Guide ist vollständig und präzise – nutze ihn.
  • Der Compiler ist dein erster Verbündeter. Lass ihn reden, bevor du anfängst zu fixen.
  • Config-Änderungen sind die tückischsten. Kein Compile-Fehler, kein Stack Trace – nur stilles Fehlverhalten. Geh die Properties bewusst durch.
  • Tests zuerst rot werden lassen, dann grün. Das ist keine Metapher, das ist der Plan.

Hibernate 7 ist kein Upgrade, das du hinauszögerst. Java 17 Baseline, Apache License, konsequente JPA-Ausrichtung – das ist die Plattform für die nächsten Jahre. Wer jetzt migriert, zahlt den Preis einmal. Wer wartet, zahlt ihn mit Zinsen.

Das war die Hibernate-7-Serie. Danke fürs Mitlesen – und viel Erfolg beim Upgrade. 🚀