Skip to main content

Hibernate 8 and Jakarta Persistence 4.0 Preview: What Will Break

A 205-test Hibernate suite run against Hibernate 8.0.0.Beta3 and Jakarta Persistence 4.0.0-M7: the classpath trap, the Spring blocker, removed APIs, a binary-compatibility break and the runtime changes that did and did not reproduce. Preview article, updated at GA.

Hibernate ORM 8 is in beta and Jakarta Persistence 4.0 is still a milestone, so nobody should ship on them. But the day both go final, every application that uses Hibernate — directly, through Spring Data, through Quarkus — has to move. This article does the dull, useful thing early: it takes an existing suite of 205 Hibernate tests, points it at Hibernate 8.0.0.Beta3, and reports what fails, why, and which failures are yours to fix versus somebody else’s. You need no Hibernate internals to follow it. If you know that an ORM maps Java objects to database rows and that JPA (Jakarta Persistence) is the standard API Hibernate implements, you have enough. Every claim points at a file in the companion repository; sections have collapsed “going deeper” panels for the details.
Versions and status. Hibernate ORM 8.0.0.Beta3 (25 Sep 2026; Beta2 23 Sep, Beta1 16 Jun) and Jakarta Persistence 4.0.0-M7 (a milestone — the only 4.0 artifacts on Maven Central are M5–M7), compared with Hibernate 7.4.5.Final and Persistence 3.2.0. Spring Boot 4.1.1 / Spring Framework 7.0.9 and, for one run, Boot 4.2.0-M2 / Spring 7.1.0-M2. JDK 25.0.4, Maven 3.9.11. Nothing here is final; this article will be updated when 8.0 is released. Companion code: hibernate-demo/h8-preview.

Start with a suite you trust: 205 tests, two pre-existing errors

A migration test is only meaningful against a baseline. The companion repository’s earlier chapters ship a JUnit suite (mapping, caching, pagination, interceptors, full-text search) that runs on Hibernate 7.4.5 under Spring Boot 4.1.1. A script copies that suite to a scratch directory, swaps the Hibernate version, runs everything, and boils the result down to distinct final causes — because 159 failing tests are not 159 problems. On Hibernate 7.4.5 the baseline is 205 tests and two errors, both in one class. I did not cause those; they fail the same way when run alone, and they are a pre-existing defect in that older chapter’s fixture (listed at the end of this article).
tests run: 205  failures: 0  errors: 2  skipped: 0
test classes with a problem: 1
distinct final causes (count = failing test methods):
     1  org.hibernate.query.UnknownNamedQueryException: No query named 'XmlQueryEmployee.findBySalaryAboveXml'
     1  org.hibernate.query.UnknownParameterException: No parameter named ':min' in query with named parameters []

Captured in suite-h7.txt.

Going deeper: how the runner works
The runner is a shell script: copy ../src and ../pom.xml to a temp directory, optionally change the Spring Boot parent version, optionally add two dependency pins, then run mvn test with -Dhibernate.version=… and -Djakarta-persistence.version=… (the two property names Spring Boot’s dependency management reads). The original tree is never modified, so the same script can compare four configurations without them contaminating each other. A note on counts: they come from the JUnit XML-style text reports, so parameterised and nested tests can make the number differ from the one Maven prints in its own summary line.

Failure 1: Maven quietly picks an older library than Hibernate 8 asks for

First run on Hibernate 8: 202 tests, 183 problems. The number is scary and the cause is boring. The suite also uses Hibernate Search 8.4, which was built for Hibernate 7 and depends on an older hibernate-models library (1.1.1). Hibernate 8 itself needs hibernate-models 1.3.4. When two dependencies ask for different versions of the same library, Maven does not take the newest; it takes the one closest to your project in the dependency tree. Hibernate Search sits one level nearer than Hibernate Core, so its older copy wins. The result is a runtime error inside Hibernate itself, not a build error:
tests run: 202  failures: 2  errors: 181  skipped: 0
test classes with a problem: 67
distinct final causes (count = failing test methods):
   140  (context failed earlier in the same JVM; see the first failure of that context)
    39  java.lang.ExceptionInInitializerError: Exception java.lang.NoSuchFieldError: Class org.hibernate.models.spi.AnnotationTarget$Kind does not have member field 'org.hibernat
     1  javax.naming.NameAlreadyBoundException: Name jdbc already bound.  Use rebind() to override
     1  org.opentest4j.AssertionFailedError: Expected javax.naming.NameAlreadyBoundException to be thrown, but nothing was thrown.
     1  java.lang.AssertionError: 

Captured in suite-h8-unpinned.txt.

Tests with a problem, same suite, four configurationsHibernate 7.4.52H8 Beta3, unpinned183H8 Beta3, pinned159H8 + Boot 4.2.0-M2159Pinning fixes the classpath, not the next problem; a newer Spring milestone does not change the count.
The chart shows what the pin buys. Fixing the classpath with two lines removes 24 problems and exposes what was hiding behind them. The pin is the only dependency change the runner makes.
    <dependency><groupId>org.hibernate.models</groupId><artifactId>hibernate-models</artifactId><version>1.3.4</version></dependency>
    <dependency><groupId>io.smallrye</groupId><artifactId>jandex</artifactId><version>3.6.0</version></dependency>

Source: run-suite.sh, lines 21–22 (the script writes them into a scratch copy of the POM, inside a dependencyManagement block).

The fingerprint of this bug. NoSuchFieldError or NoSuchMethodError naming a class in org.hibernate.models means two versions of the same library are on the classpath. Run mvn dependency:tree -Dincludes=org.hibernate.models before you suspect Hibernate.

Failure 2: Spring cannot host Hibernate 8 yet, and that is not something you can fix

With the classpath fixed, 159 tests still have a problem. 140 of them only report that a Spring context failed earlier; among the tests that show their own cause, the biggest group has one. Jakarta Persistence 4.0 adds six abstract methods to PersistenceUnitInfo, the interface a container uses to describe a persistence unit to the provider. Hibernate 8 calls them. Spring’s implementation, SpringPersistenceUnitInfo, does not have them, so the entity manager factory cannot be created at all:
tests run: 205  failures: 1  errors: 158  skipped: 0
test classes with a problem: 52
distinct final causes (count = failing test methods):
   140  (context failed earlier in the same JVM; see the first failure of that context)
    14  java.lang.NoSuchMethodException: org.springframework.orm.jpa.persistenceunit.SpringPersistenceUnitInfo.getAllPackageDescriptors()
     4  org.hibernate.query.NamedQueryValidationException: Errors in named queries: 
     1  java.lang.AssertionError: 

Captured in suite-h8-pinned.txt.

Where the gap isYour application (@Entity, repositories)Spring Boot 4.1.1 auto-configurationSpring ORM 7.0.9 / 7.1.0-M2SpringPersistenceUnitInfoJakarta Persistence 4.0.0-M7PersistenceUnitInfo: six new abstract methodsHibernate ORM 8.0.0.Beta3 calls themThe red box implements the old interface. It sits between your code and Hibernate, so no application-side change reaches it.Spring 7.1.0-M2 (the milestone under Boot 4.2.0-M2) does not have the methods either.
The javap comparison of the two interface versions and of Spring’s class is committed as a transcript. The list is what changed in the interface; the last four lines show Spring’s class was found and implements none of them, in both the current release and the 7.1 milestone.
# persistence-spi (javap on jakarta.persistence.spi.PersistenceUnitInfo, and on Spring's implementation)
--- methods that exist in Jakarta Persistence 4.0.0-M7 but not in 3.2.0
  public abstract FetchType getDefaultToOneFetchType();
  public abstract List<String> getAllClassNames();
  public abstract List<String> getAllModuleDescriptors();
  public abstract List<String> getAllPackageDescriptors();
  public abstract List<String> getManagedModuleDescriptors();
  public abstract List<String> getManagedPackageDescriptors();
--- org.springframework.orm.jpa.persistenceunit.SpringPersistenceUnitInfo 7.0.9: methods mentioning those names
  class found, 15 methods and constructors listed; matches for the six new names: 0
--- org.springframework.orm.jpa.persistenceunit.SpringPersistenceUnitInfo 7.1.0-M2: methods mentioning those names
  class found, 15 methods and constructors listed; matches for the six new names: 0

Captured in 06-persistence-spi.txt.

What this means. A Spring Boot application cannot run Hibernate 8 today, whatever you put in the POM. The fix has to land in Spring Framework (probably 7.1 or later). If you use Spring Data JPA, keep an eye on the Framework milestones, not on Hibernate’s. Hibernate used without Spring is the only setup the rest of this article can exercise.
Going deeper: what the 140, the 14 and the 4 are
Of the 159 problems, 140 are follow-ons: Spring caches a failed application context and refuses to retry it in the same JVM, so every later test in the same configuration reports “failure threshold exceeded”. That is why the runner reports distinct final causes. The other 19 have three causes: 14 hit the missing getAllPackageDescriptors(), 4 come from the named-query validation described in the last panel of this article, and 1 is the AssertionError below. The one AssertionError in the list is a test that inspected XML mapping metadata; I did not chase it further, so treat it as unexplained.

Failure 3: code that compiled on Hibernate 7 no longer does

Everything so far was infrastructure. This section is about your code. Five tiny files, none in the normal build, are compiled against each Hibernate and the compiler’s words are kept. First, APIs that Hibernate 7 already marked deprecated and Hibernate 8 has removed. Here the same file gives two warnings on 7 and a hard error on 8:
    public static class Dto {
        public Integer id;
    }

    Object rows(Session s) {
        return s.createNativeQuery("select 1 as id", Object.class).unwrap(NativeQuery.class)
                .setResultTransformer(Transformers.aliasToBean(Dto.class)).list();
    }
}

Source: UsesTransformers.java, lines 8–17.

--- UsesTransformers on h8
UsesTransformers.java:3: error: package org.hibernate.transform does not exist
import org.hibernate.transform.Transformers;
UsesTransformers.java:14: error: cannot find symbol
				.setResultTransformer(Transformers.aliasToBean(Dto.class)).list();
  symbol:   variable Transformers
  location: class UsesTransformers
2 errors

Captured in 04-compile-errors.txt.

Result transformers (org.hibernate.transform) were the way to map a native query onto a DTO; the whole package is gone. Session.replicate and ReplicationMode fail the same way (lines 26–35 of the transcript). A different kind of break is a changed return type. A bulk-loading line many projects contain keeps the generated id:
    Object load(StatelessSession ss) {
        Object id = ss.insert(new com.ankurm.h8.Author("A", "[email protected]"));
        return id;
    }

Source: StatelessInsertReturnsId.java, lines 6–9.

--- StatelessInsertReturnsId on h7
--- StatelessInsertReturnsId on h8
StatelessInsertReturnsId.java:7: error: incompatible types: void cannot be converted to Object
		Object id = ss.insert(new com.ankurm.h8.Author("A", "[email protected]"));
1 error

Captured in 04-compile-errors.txt.

Read this one twice. A method whose return type changes is worse than one that disappears: the line above still looks perfectly normal. Grep for StatelessSession and any code that uses the value returned by insert(..).
Going deeper: the removal list, and what I did not compile
The Hibernate 8 migration guide lists these removals: Session.replicate() with ReplicationMode; the whole org.hibernate.transform package; the lock cascade style; the @Proxy and @Polymorphism XML attributes; OSGi manifest metadata; and the relocation POMs under the old org.hibernate group id. I compiled the first two and the StatelessSession change. The rest come from the guide, not from a run.

The change that does not show up in your code: binary compatibility

A very common line still compiles on Hibernate 8, with only a warning: Query<Author> q = session.createQuery(hql, Author.class). That is source compatibility. It is not binary compatibility: the return type of Session.createQuery(String, Class) changed underneath, and the compiled call site records the return type. The probe below is compiled against one Hibernate and run on the other, in both directions.
    int count(Session s) {
        Query<com.ankurm.h8.Author> q = s.createQuery("from Author", com.ankurm.h8.Author.class);
        return q.getResultList().size();
    }
}

Source: QueryVariable.java, lines 7–11.

--- QueryVariable on h8
QueryVariable.java:9: warning: [removal] getResultList() in Query has been deprecated and marked for removal
		return q.getResultList().size();
  where T is a type-variable:
    T extends Object declared in interface Query
1 warning

Captured in 04-compile-errors.txt.

# binary-compatibility (BinaryProbe compiled against one Hibernate, run on the other)
--- compiled on h7, run on h8
Exception in thread "main" java.lang.NoSuchMethodError: 'org.hibernate.query.Query org.hibernate.Session.createQuery(java.lang.String, java.lang.Class)'
--- compiled on h8, run on h7
Exception in thread "main" java.lang.NoSuchMethodError: 'org.hibernate.query.SelectionQuery org.hibernate.Session.createQuery(java.lang.String, java.lang.Class)'

Captured in 05-binary-compatibility.txt.

Why this matters more than the compile errors. You can fix your own source. You cannot recompile spring-data-jpa, an in-house library, or any vendor jar built against Hibernate 7. Every such jar that calls createQuery(String, Class) throws NoSuchMethodError on Hibernate 8 until its authors rebuild it. That, not your entity code, sets your real migration date.

What changed at runtime, and what the migration guide says that I could not reproduce

The migration guide lists behaviour changes. Three of them can be observed with plain Hibernate, so I ran identical code on both versions. Two behaviours differ. The rest did not. Flush order. Hibernate 8 sorts pending inserts by their foreign-key graph. I persisted a parent, its child, another parent and its child, in that order, and recorded the order in which Hibernate prepares INSERT statements:
    private static List<String> run(Map<String, String> settings) {
        Db.SQL.clear();
        try (SessionFactory sf = Db.open(settings, Parent.class, Child.class)) {
            Db.SQL.clear();
            sf.inTransaction(s -> {
                Parent p1 = new Parent("p1");
                Parent p2 = new Parent("p2");
                s.persist(p1);
                s.persist(new Child("c1", p1));
                s.persist(p2);
                s.persist(new Child("c2", p2));
            });
            return Db.SQL.stream().filter(q -> q.startsWith("insert")).map(q -> q.split(" ")[2]).toList();
        }
    }

Source: FlushOrderTest.java, lines 13–27.

# 02-flush-order  (Hibernate ORM 7.4.5.Final, JDK 25)
default                      : [Parent, Child, Parent, Child]
hibernate.flush.queue.type=legacy: [Parent, Child, Parent, Child]
hibernate.flush.queue.type=graph : [Parent, Child, Parent, Child]

Captured in 02-flush-order-h7.txt.

# 02-flush-order  (Hibernate ORM 8.0.0.Beta3, JDK 25)
default                      : [Parent, Child]
hibernate.flush.queue.type=legacy: [Parent, Child, Parent, Child]
hibernate.flush.queue.type=graph : [Parent, Child]

Captured in 02-flush-order-h8.txt.

On 7 the four statements alternate parent, child, parent, child, whatever the setting. On 8 the default groups them (parents together, then children), and hibernate.flush.queue.type=legacy brings the old alternating order back. The effect is fewer switches between tables, but SQL logs and any test that asserts statement order will change. A mutating @NamedQuery. The guide says named queries that modify data must use the new @NamedStatement. This entity has a JPA @NamedQuery whose text is an update:
/** Not registered by default: a named query that is an UPDATE. Hibernate 8 wants @NamedStatement for this. */
@Entity
@NamedQuery(name = "Mutating.rename", query = "update MutatingNamedQueryEntity m set m.name = :name")
public class MutatingNamedQueryEntity {

    @Id
    Long id;

    String name;
}

Source: MutatingNamedQueryEntity.java, lines 7–16.

# 03-mutating-named-query  (Hibernate ORM 7.4.5.Final, JDK 25)
session factory built; createNamedQuery(..).executeUpdate() returned 0

Captured in 03-mutating-named-query-h7.txt.

# 03-mutating-named-query  (Hibernate ORM 8.0.0.Beta3, JDK 25)
threw java.lang.IllegalArgumentException <- org.hibernate.query.IllegalSelectQueryException :: Expecting a SELECT Query [org.hibernate.query.sqm.tree.spi.select.SqmSelectStatement], but found org.hibernate.query.sqm.tree.spi.update.SqmUpdateStatement [update MutatingNamedQueryEntity m set m.name = :name]

Captured in 03-mutating-named-query-h8.txt.

Hibernate 7 accepts it and runs the update. Hibernate 8 refuses to build the SessionFactory, so this is a startup failure, not something a test hits later. Any application with a modifying @NamedQuery will not start until it moves to @NamedStatement. Exceptions. The guide lists several exception-type changes. Five ordinary situations, same code, both versions:
# 01-exception-contracts  (Hibernate ORM 7.4.5.Final, JDK 25)
unbound parameter  : threw org.hibernate.QueryParameterException
null parameter     : returned 0
type mismatch      : threw org.hibernate.query.QueryArgumentException
unique violation   : threw org.hibernate.exception.ConstraintViolationException <- org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException
unknown parameter  : threw java.lang.IllegalArgumentException <- org.hibernate.query.UnknownParameterException
named query, no row: 0 rows

Captured in 01-exception-contracts-h7.txt.

# 01-exception-contracts  (Hibernate ORM 8.0.0.Beta3, JDK 25)
unbound parameter  : threw org.hibernate.QueryParameterException
null parameter     : returned 0
type mismatch      : threw org.hibernate.query.QueryArgumentException
unique violation   : threw org.hibernate.exception.ConstraintViolationException <- org.h2.jdbc.JdbcBatchUpdateException
unknown parameter  : threw java.lang.IllegalArgumentException <- org.hibernate.query.UnknownParameterException
named query, no row: 0 rows

Captured in 01-exception-contracts-h8.txt.

Guide saysI observed on 8.0.0.Beta3
Null query parameter throws IllegalStateExceptionsetParameter("name", null) returned 0 rows, same as 7
Unique-constraint violation throws EntityExistsExceptionConstraintViolationException, same class as 7 (the root cause is H2’s batch exception on 8, its integrity-constraint exception on 7)
Type mismatch throws IllegalArgumentExceptionQueryArgumentException, same class as 7 — I did not check its supertype
Take that table as “not reproduced with these inputs”, not as “the guide is wrong”. The guide may describe other code paths (for example a null argument to an API method rather than a null bound value). The exact wording matters and I only tested the obvious reading.
Going deeper: two more behaviours the suite exposed
Named queries in a default orm.xml. The older chapters keep a META-INF/orm.xml whose named queries refer to one entity. Persistence units bootstrapped outside Spring (four tests in the suite) load it automatically. On 7.4.5 they start. On 8.0.0.Beta3 they fail at startup with NamedQueryValidationException: Could not resolve root entity, because those units do not list that entity. I read that as Hibernate 8 validating more strictly, but I did not confirm it against the release notes, so it is an observation, not an explanation. Hibernate Search. Search 8.4 is built for Hibernate 7; the suite’s search tests fail for the reasons above. Expect a Search release aligned with Hibernate 8 before you can test them.

From the migration guide, not tested here

These are in the guide and I did not exercise them. They are listed so you know what to look for; each is a claim from the guide, not from a run.
AreaChange (migration guide)
Java timehibernate.type.java_time_use_direct_jdbc defaults to true
IDs@NativeGenerator default allocation size changes from 1 to 50
LockingLock cascading removed; explicit locking required
Large objectsBlob/Clob access requires an active transaction
Mappinghibernate.mapping.default_list_semantics replaced by @DefaultListSemantics; @OptimisticLock replaced by @ExcludedFromVersioning
SessionsRe-entering the session from a callback throws IllegalStateException
Micrometermicrometer-core is no longer a transitive dependency of hibernate-micrometer

Should you test Hibernate 8 now?

Yes, but only the part you control. Compile your code against 8.0.0.Beta3 to size the source changes, and check every third-party jar that touches Hibernate for a release compiled against 8. Do not try to run a Spring Boot application on it: that is blocked until Spring implements the new Persistence SPI. And do not migrate on a beta — the behaviours above are exactly the kind that move between Beta3 and the final release.

Pre-existing defects noticed on the way

Reported separately because they are not part of the Hibernate 8 story.
  • OrmXmlNamedQueryTest in the main hibernate-demo suite has two errors on Hibernate 7.4.5 too (UnknownNamedQueryException for XmlQueryEmployee.findBySalaryAboveXml; reproduces when the class is run alone).

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.