Skip to main content

Hibernate 5 to 7 Migration Guide and Cheat Sheet: What Breaks, What Changed, What to Write Now

Hibernate 5.6.15 and 7.4.14 compared jar by jar and run side by side: removed APIs, annotation changes, the schema defaults that moved, and a cheat sheet of snippets verified on 7.4.14.

Moving from Hibernate 5 to Hibernate 7 is two upgrades in one: Hibernate 6 rewrote the query engine and the type system, and Hibernate 7 removed what 6 had deprecated. Instead of retelling the migration guides I put Hibernate 5.6.15 and 7.4.14 side by side: the two hibernate-core jars compared class by class, Hibernate 5 code compiled against both, the same entity mapped on both, and a Hibernate 7 schema check against a database that Hibernate 5 created. The last part is a cheat sheet of snippets I ran on 7.4.14.

Versions. Before: Hibernate ORM 5.6.15.Final (org.hibernate:hibernate-core, javax.persistence). After: 7.4.14.Final (org.hibernate.orm:hibernate-core, jakarta.persistence 3.2.0). Database: H2 2.4.240. Compiled with JDK 25 using --release 17. Not covered: Spring Boot and Spring Data, Envers, Hibernate Search, second-level caching, multi-tenancy, XML mappings, bytecode enhancement and every database other than H2. Hibernate 5.6.15 runs here with a newer Byte Buddy than it ships with, because the bundled one does not recognise the JDK.

The code is one module of the companion repository: hibernate-demo/migration-5-to-7. Every code block and every line of output below is a real file in it, linked under the block. The explanations stay in this post.

What changes before you touch any code

Three things change in the build file. The Maven group id moved, the Java baseline moved, and the persistence namespace moved. The first two come from reading the jars.

            5.x                                           7.x
coordinates org.hibernate:hibernate-core:5.6.15.Final     org.hibernate.orm:hibernate-core:7.4.14.Final
class files Java 8                                        Java 17
persistence javax.persistence (JPA 2.2)                   jakarta.persistence (Jakarta Persistence 3.2)

Output: 01-versions-and-baseline.txt, written by api_diff.py, which downloads the jars from Maven Central.

The namespace move I checked by compiling one empty entity twice, once with javax.persistence and once with jakarta.persistence, against each version:

Hibernate 5.6.15, compiling JavaxNamespace.java
javac exit status: 0
Hibernate 7.4.14, compiling JavaxNamespace.java
src/compat/JavaxNamespace.java:1: error: package javax.persistence does not exist
import javax.persistence.Entity;
                        ^
src/compat/JavaxNamespace.java:3: error: cannot find symbol
@Entity
 ^
  symbol: class Entity
2 errors
javac exit status: 1
Hibernate 5.6.15, compiling JakartaNamespace.java
src/compat/JakartaNamespace.java:1: error: package jakarta.persistence does not exist
import jakarta.persistence.Entity;
                          ^
src/compat/JakartaNamespace.java:3: error: cannot find symbol
@Entity
 ^
  symbol: class Entity
2 errors
javac exit status: 1
Hibernate 7.4.14, compiling JakartaNamespace.java
javac exit status: 0

Output: 09-namespace-javax-5.6.15.txt, 09-namespace-javax-7.4.14.txt, 09-namespace-jakarta-5.6.15.txt and 09-namespace-jakarta-7.4.14.txt. Each version accepts exactly one of the two. There is no setting that makes 7.4.14 read javax.persistence annotations: every import javax.persistence in your code has to change. An automated rewrite does that; see Automating javax to jakarta and Boot 3 to 4 Upgrades with OpenRewrite.

Compile first: Hibernate 5 calls against both versions

I wrote one class with the Hibernate 5 calls that most older code still contains. It uses no persistence annotations, so the namespace change does not hide the real errors.

Show OldHibernate.java
import java.io.Serializable;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;

/** Hibernate 5 style calls that use no persistence annotations. Compiled by scripts/compile-old-apis.sh against 5.6.15 and 7.4.14. */
@SuppressWarnings({"deprecation", "rawtypes"})
class OldHibernate {

    Serializable save(Session s, Object o) {
        return s.save(o);
    }

    void update(Session s, Object o) {
        s.update(o);
    }

    void saveOrUpdate(Session s, Object o) {
        s.saveOrUpdate(o);
    }

    void delete(Session s, Object o) {
        s.delete(o);
    }

    Object load(Session s) {
        return s.load(Object.class, 1L);
    }

    List criteria(Session s) {
        Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
        return c.list();
    }

    List sql(Session s) {
        return s.createSQLQuery("select 1").list();
    }

    void flushMode(Session s) {
        s.setFlushMode(FlushMode.AUTO);
    }

    Object metadata(SessionFactory sf) {
        return sf.getClassMetadata(Object.class);
    }
}

Source: OldHibernate.java.

Hibernate 5.6.15, compiling OldHibernate.java
javac exit status: 0

Output: 03-old-hibernate-apis-5.6.15.txt. It compiles on 5.6.15. The same file on 7.4.14:

Hibernate 7.4.14, compiling OldHibernate.java
src/compat/OldHibernate.java:4: error: cannot find symbol
import org.hibernate.Criteria;
                    ^
  symbol:   class Criteria
  location: package org.hibernate
src/compat/OldHibernate.java:8: error: package org.hibernate.criterion does not exist
import org.hibernate.criterion.Restrictions;
                              ^
src/compat/OldHibernate.java:15: error: cannot find symbol
        return s.save(o);
                ^
  symbol:   method save(Object)
  location: variable s of type Session
src/compat/OldHibernate.java:19: error: cannot find symbol
        s.update(o);
         ^
  symbol:   method update(Object)
  location: variable s of type Session
src/compat/OldHibernate.java:23: error: cannot find symbol
        s.saveOrUpdate(o);
         ^
  symbol:   method saveOrUpdate(Object)
  location: variable s of type Session
src/compat/OldHibernate.java:27: error: cannot find symbol
        s.delete(o);
         ^
  symbol:   method delete(Object)
  location: variable s of type Session
src/compat/OldHibernate.java:31: error: incompatible types: void cannot be converted to Object
        return s.load(Object.class, 1L);
                     ^
src/compat/OldHibernate.java:35: error: cannot find symbol
        Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
        ^
  symbol:   class Criteria
  location: class OldHibernate
src/compat/OldHibernate.java:35: error: cannot find symbol
        Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
                      ^
  symbol:   method createCriteria(Class<Object>)
  location: variable s of type Session
src/compat/OldHibernate.java:35: error: cannot find symbol
        Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
                                                        ^
  symbol:   variable Restrictions
  location: class OldHibernate
src/compat/OldHibernate.java:40: error: cannot find symbol
        return s.createSQLQuery("select 1").list();
                ^
  symbol:   method createSQLQuery(String)
  location: variable s of type Session
src/compat/OldHibernate.java:44: error: incompatible types: FlushMode cannot be converted to FlushModeType
        s.setFlushMode(FlushMode.AUTO);
                                ^
src/compat/OldHibernate.java:48: error: cannot find symbol
        return sf.getClassMetadata(Object.class);
                 ^
  symbol:   method getClassMetadata(Class<Object>)
  location: variable sf of type SessionFactory
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
13 errors
javac exit status: 1

Output: 03-old-hibernate-apis-7.4.14.txt. Thirteen errors, and every one is a real removal:

Hibernate 5 callOn 7.4.14Replacement (compiles on both)
session.save(o)gonesession.persist(o)
session.update(o), saveOrUpdate(o)gonesession.merge(o), which returns the managed copy
session.delete(o)gonesession.remove(o)
session.load(Class, id)no longer returns the entitysession.getReference(Class, id) or find
session.createCriteria(...), org.hibernate.criterion.*package and class gonean HQL query: createQuery(hql, Class)
session.createSQLQuery(sql)gonesession.createNativeQuery(sql)
session.setFlushMode(FlushMode)now takes FlushModeTypesession.setHibernateFlushMode(FlushMode)
sessionFactory.getClassMetadata(...)gonenot verified

The replacements in the right-hand column are not an opinion: this class compiled on both versions, so you can make these changes before the upgrade, while the old code still runs.

Show NewHibernate.java and its compile results
import java.util.List;

import org.hibernate.FlushMode;
import org.hibernate.Session;

/** The replacements for OldHibernate that exist on both 5.6.15 and 7.4.14, so they can be made before the upgrade. */
class NewHibernate {

    void persist(Session s, Object o) {
        s.persist(o);
    }

    Object merge(Session s, Object o) {
        return s.merge(o);
    }

    void remove(Session s, Object o) {
        s.remove(o);
    }

    Object find(Session s) {
        return s.find(Object.class, 1L);
    }

    Object reference(Session s) {
        return s.getReference(Object.class, 1L);
    }

    List<Object> query(Session s) {
        return s.createQuery("from Object o where o.name = :n", Object.class).setParameter("n", "x").getResultList();
    }

    List<?> nativeQuery(Session s) {
        return s.createNativeQuery("select 1").getResultList();
    }

    void flushMode(Session s) {
        s.setHibernateFlushMode(FlushMode.AUTO);
    }
}

Source: NewHibernate.java.

Hibernate 5.6.15, compiling NewHibernate.java
javac exit status: 0
Hibernate 7.4.14, compiling NewHibernate.java
src/compat/NewHibernate.java:34: warning: [deprecation] createNativeQuery(String) in Session has been deprecated
        return s.createNativeQuery("select 1").getResultList();
                ^
1 warning
javac exit status: 0

Output: 04-new-hibernate-apis-5.6.15.txt and 04-new-hibernate-apis-7.4.14.txt. On 7.4.14 the one-argument createNativeQuery compiles with a deprecation warning, so prefer the overload that takes a result class once you are there.

Annotation attributes

The same exercise for Hibernate’s own annotations, applied to a plain class:

Show OldAnnotations.java and its compile results
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.Where;

/** Hibernate 5 annotation attributes. The annotations are applied to a plain class, so no persistence import is needed. */
@SuppressWarnings("deprecation")
@Where(clause = "deleted = false")
class OldAnnotations {

    @Type(type = "yes_no")
    boolean flag;

    @GenericGenerator(name = "g", strategy = "increment")
    Long id;
}

Source: OldAnnotations.java.

Hibernate 5.6.15, compiling OldAnnotations.java
javac exit status: 0
Hibernate 7.4.14, compiling OldAnnotations.java
src/compat/OldAnnotations.java:3: error: cannot find symbol
import org.hibernate.annotations.Where;
                                ^
  symbol:   class Where
  location: package org.hibernate.annotations
src/compat/OldAnnotations.java:7: error: cannot find symbol
@Where(clause = "deleted = false")
 ^
  symbol: class Where
src/compat/OldAnnotations.java:10: error: cannot find symbol
    @Type(type = "yes_no")
          ^
  symbol:   method type()
  location: @interface Type
src/compat/OldAnnotations.java:10: error: annotation @Type is missing a default value for the element 'value'
    @Type(type = "yes_no")
    ^
src/compat/OldAnnotations.java:13: warning: [removal] GenericGenerator in org.hibernate.annotations has been deprecated and marked for removal
    @GenericGenerator(name = "g", strategy = "increment")
     ^
src/compat/OldAnnotations.java:13: warning: [removal] strategy() in GenericGenerator has been deprecated and marked for removal
    @GenericGenerator(name = "g", strategy = "increment")
                                  ^
4 errors
2 warnings
javac exit status: 1

Output: 05-old-annotations-5.6.15.txt and 05-old-annotations-7.4.14.txt.

On 7.4.14 @Where is gone, @Type no longer has a type string attribute, and @GenericGenerator still compiles but its strategy is deprecated and marked for removal. The Hibernate 6 and 7 spelling of the first idea compiles on 7.4.14 and, as expected, not on 5.6.15:

Show NewAnnotations.java and its compile results
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.SQLRestriction;
import org.hibernate.type.SqlTypes;

/** The Hibernate 6 and 7 spelling of the same ideas. This does not compile on 5.6.15. */
@SQLRestriction("deleted = false")
class NewAnnotations {

    @JdbcTypeCode(SqlTypes.CHAR)
    boolean flag;
}

Source: NewAnnotations.java.

Hibernate 5.6.15, compiling NewAnnotations.java
src/compat/NewAnnotations.java:1: error: cannot find symbol
import org.hibernate.annotations.JdbcTypeCode;
                                ^
  symbol:   class JdbcTypeCode
  location: package org.hibernate.annotations
src/compat/NewAnnotations.java:2: error: cannot find symbol
import org.hibernate.annotations.SQLRestriction;
                                ^
  symbol:   class SQLRestriction
  location: package org.hibernate.annotations
src/compat/NewAnnotations.java:3: error: cannot find symbol
import org.hibernate.type.SqlTypes;
                         ^
  symbol:   class SqlTypes
  location: package org.hibernate.type
src/compat/NewAnnotations.java:6: error: cannot find symbol
@SQLRestriction("deleted = false")
 ^
  symbol: class SQLRestriction
src/compat/NewAnnotations.java:9: error: cannot find symbol
    @JdbcTypeCode(SqlTypes.CHAR)
     ^
  symbol:   class JdbcTypeCode
  location: class NewAnnotations
src/compat/NewAnnotations.java:9: error: cannot find symbol
    @JdbcTypeCode(SqlTypes.CHAR)
                  ^
  symbol:   variable SqlTypes
  location: class NewAnnotations
6 errors
javac exit status: 1
Hibernate 7.4.14, compiling NewAnnotations.java
javac exit status: 0

Output: 06-new-annotations-5.6.15.txt and 06-new-annotations-7.4.14.txt. @SQLRestriction takes the place of @Where. The @JdbcTypeCode line only shows that the annotation exists: I did not check which JDBC type reproduces what @Type(type = "yes_no") stored, so look that up before you replace it.

The change the compiler cannot see: what Hibernate creates in your database

This is the part that bites in production. I mapped one entity with a generated id and one field of each commonly used type, once with javax.persistence on 5.6.15 and once with jakarta.persistence on 7.4.14, let Hibernate create the schema in H2, and read the result back from information_schema.

Show the entity (Hibernate 7 version)
package com.ankurm.migration;

import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.UUID;

/** One field of each type whose default column mapping is worth comparing, and a generated id. */
@Entity
public class Probe {

    @Id
    @GeneratedValue
    Long id;

    String name;
    boolean active;
    Instant createdAt;
    LocalDateTime occurredAt;
    UUID uuid;
    Duration span;
    BigDecimal price;
    byte[] payload;

    @Enumerated(EnumType.ORDINAL)
    Colour byOrdinal;

    @Enumerated(EnumType.STRING)
    Colour byName;

    public enum Colour { RED, GREEN }

    public Probe() {
    }

    Probe(String name) {
        this.name = name;
    }
}

Source: Probe.java. The 5.6.15 module has the same class with javax.persistence imports.

              Hibernate 5.6.15          Hibernate 7.4.14          
-- id sequences (name, increment)
              HIBERNATE_SEQUENCE 1      PROBE_SEQ 50
-- column types in H2
ACTIVE        BOOLEAN                   BOOLEAN                   (same)
BYNAME        CHARACTER VARYING         ENUM                      <-- differs
BYORDINAL     INTEGER                   TINYINT                   <-- differs
CREATEDAT     TIMESTAMP                 TIMESTAMP WITH TIME ZONE  <-- differs
ID            BIGINT                    BIGINT                    (same)
NAME          CHARACTER VARYING         CHARACTER VARYING         (same)
OCCURREDAT    TIMESTAMP                 TIMESTAMP                 (same)
PAYLOAD       BINARY VARYING            BINARY VARYING            (same)
PRICE         NUMERIC                   NUMERIC                   (same)
SPAN          BIGINT                    NUMERIC                   <-- differs
UUID          BINARY                    UUID                      <-- differs

Output: 06-schema-diff.txt, lined up by schema_diff.py from 06-schema-5.6.15.txt and 06-schema-7.4.14.txt. Six rows differ:

  • Ids. A bare @GeneratedValue used one shared sequence with increment 1 on 5.6.15 and uses one sequence per entity with increment 50 on 7.4.14.
  • Enums. An EnumType.ORDINAL column is a TINYINT instead of an INTEGER, and an EnumType.STRING column is an ENUM instead of a VARCHAR (on H2).
  • Time and UUID. Instant becomes TIMESTAMP WITH TIME ZONE, UUID becomes UUID instead of BINARY, and Duration is a NUMERIC instead of a BIGINT.

These are H2’s names for the types; other databases will name them differently, and I tested none of them. What matters is that the defaults moved. If you let Hibernate validate an existing schema, it notices. I created the database with Hibernate 5.6.15 (into an H2 file) and opened it with Hibernate 7.4.14 and the same entity:

Hibernate 7.4.14 opening the H2 file database written by Hibernate 5.6.15 (same Probe entity)
hbm2ddl.auto=validate: failed: SchemaManagementException: Schema validation: wrong column type encountered in column [span] in table [Probe]; found [bigint (Types#BIGINT)], but expecting [numeric(21,0) (Types#NUMERIC)]
hbm2ddl.auto=none    : opened; loaded the row written by 5.6.15: name=legacy row, uuid=null

Output: 08-validate-against-5.6.15-schema-7.4.14.txt, from ValidateLegacySchemaTest.java (the schema is written by LegacySchemaTest.java). With validate the application refuses to start, and the message names one column; fixing it reveals the next. With validation off the application opens and reads the row, so the mismatch would only show up when a value is actually read or written in a column whose type changed. Plan the schema work as part of the upgrade: either keep the old types with explicit annotations, or migrate the columns.

Limits. One entity, one database, no data conversion. I did not test what 7.4.14 does when it reads a BINARY UUID, how it treats existing ordinal values, or the id sequences against rows that already exist (the sequence names and increments differ, so check them before the first insert).

Queries: two things that did not change

Two HQL habits are often blamed on the upgrade, so I ran them on both versions. The probe also records the outcome of save and createCriteria on 5.6.15, which is the last time they work.

Hibernate 5.6.15
session.save(...) returned: 1 (Long)
HQL with ?1                      : 1 row(s)
HQL with a bare ?                : failed: IllegalArgumentException: org.hibernate.QueryException: Legacy-style query parameters (`?`) are no longer supported; use JPA-style ordinal parameters (e.g., `?1`) ins...
HQL enum compared with 0         : 0 row(s)
session.createCriteria(...)      : 1 row(s)
Hibernate 7.4.14
session.persist(...) assigned the id: 1 (Long)
HQL with ?1                      : 1 row(s)
HQL with a bare ?                : failed: IllegalArgumentException: org.hibernate.query.ParameterLabelException: Unlabeled ordinal parameter ('?' rather than ?1)
HQL enum compared with 0         : 0 row(s)

Output: 07-legacy-queries-5.6.15.txt and 07-legacy-queries-7.4.14.txt. A bare ? parameter is already rejected on 5.6.15 and is still rejected on 7.4.14 (the message differs), and comparing an enum attribute with 0 returned no error on either. So if a query breaks in your upgrade, it is not from these two. Use ?1 or a named parameter.

Everything else that was removed

Those were my picks. To find the rest I compared every public class and member of the two hibernate-core jars. javax.persistence is read as jakarta.persistence so the namespace move does not count, and packages with internal or spi in the name, the SQL AST and the parser are left out because they were never the API you call.

== hibernate-core: public classes missing: 828, members removed from classes that remain: 2735, new classes: 5879
  of the missing classes: 59 were deprecated in 5.6.15, 137 have a class of the same simple name somewhere in 7.4.14
  packages with the most missing classes:
      75  org.hibernate.dialect
      74  org.hibernate.type
      62  org.hibernate.type.descriptor.java
      59  org.hibernate.criterion
      53  org.hibernate.cfg
      36  org.hibernate.type.descriptor.sql
      35  org.hibernate.annotations
      27  org.hibernate.dialect.function
      25  org.hibernate.sql.ordering.antlr
      23  org.hibernate.sql
      23  org.hibernate.tool.hbm2ddl
      22  org.hibernate.cfg.annotations

Output: 02-removed-api-summary.txt. Of 828 public classes that do not exist under the same name, 59 had been deprecated in 5.6.15 and 137 have a class with the same simple name elsewhere in 7.4.14, which usually means they moved package (the dialect and type descriptor packages are the big ones). The complete list, with a mark for each class that has a namesake, is 02-removed-api.txt.

Treat the 2,735 member lines as a list to check, not a list of deletions. A method counts as removed when its exact signature is gone, so changing a parameter from Serializable to Object shows up as a removal even though the method still exists. The compile checks above are the arbiter, and they are why the table earlier lists only what the compiler confirmed.

Going deeper

  • api_diff.py compares any two hibernate-core versions: change OLD and NEW at the top.
  • The official migration guides for 6.0 and 7.0 explain the reasons behind the changes; I only measured them.

Cheat sheet: snippets that ran on Hibernate 7.4.14

Every snippet below is a test in one class, CheatSheetTest.java, that prints what it did and asserts the same values. The output under each heading is the captured file. The entities are small: Author.java (with a natural id), Book.java, Note.java (soft delete) and Shop.java (a record as embeddable).

Show CheatSheetTest.java
package com.ankurm.migration;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.StatelessSession;
import org.junit.jupiter.api.Test;

/** Snippets for the cheat sheet. Each test prints what it did to ../output/1N-cheat-*.txt and asserts the same values. */
class CheatSheetTest {

    @Test
    void sessionApi() {
        try (Db db = new Db(Author.class, Book.class); Transcript t = new Transcript("10-cheat-session-api")) {
            Long id;
            try (Session s = db.sessionFactory.openSession()) {
                s.beginTransaction();
                Author a = new Author("[email protected]", "Ann");
                s.persist(a);
                id = a.id;
                s.getTransaction().commit();
                t.line("persist(a)               -> id assigned on the object: " + id);
            }
            try (Session s = db.sessionFactory.openSession()) {
                db.clear();
                Author found = s.find(Author.class, id);
                t.line("find(Author, id)         -> " + found.name + "  (" + db.sql.size() + " select)");
                assertThat(found.name).isEqualTo("Ann");
                assertThat(db.sql).hasSize(1);
            }
            try (Session s = db.sessionFactory.openSession()) {
                db.clear();
                Author ref = s.getReference(Author.class, id);
                t.line("getReference(Author, id) -> initialized: " + Hibernate.isInitialized(ref) + "  (" + db.sql.size() + " selects)");
                assertThat(Hibernate.isInitialized(ref)).isFalse();
                assertThat(db.sql).isEmpty();
            }
            try (Session s = db.sessionFactory.openSession()) {
                Author detached = new Author("[email protected]", "Ann Smith");
                detached.id = id;
                s.beginTransaction();
                Author managed = s.merge(detached);
                t.line("merge(detached)          -> returns a different instance: " + (managed != detached) + ", name now " + managed.name);
                s.getTransaction().commit();
                assertThat(managed).isNotSameAs(detached);
                assertThat(managed.name).isEqualTo("Ann Smith");
            }
            try (Session s = db.sessionFactory.openSession()) {
                s.beginTransaction();
                Author extra = new Author("[email protected]", "Bob");
                s.persist(extra);
                s.getTransaction().commit();
                Long bobId = extra.id;
                List<Author> many = s.findMultiple(Author.class, List.of(id, bobId, 999L));
                t.line("findMultiple(ids 3)      -> " + many.size() + " entries, missing id gives: " + many.get(2));
                assertThat(many).hasSize(3);
                assertThat(many.get(2)).isNull();
            }
            try (Session s = db.sessionFactory.openSession()) {
                s.beginTransaction();
                Author a = s.find(Author.class, id);
                s.remove(a);
                s.getTransaction().commit();
                t.line("remove(a)                -> find afterwards: " + s.find(Author.class, id));
                assertThat(s.find(Author.class, id)).isNull();
            }
        }
    }

    @Test
    void hql() {
        try (Db db = new Db(Author.class, Book.class); Transcript t = new Transcript("11-cheat-hql")) {
            try (Session s = db.sessionFactory.openSession()) {
                s.beginTransaction();
                Author ann = new Author("[email protected]", "Ann");
                Author bob = new Author("[email protected]", "Bob");
                s.persist(ann);
                s.persist(bob);
                s.persist(new Book("Alpha", 100, ann));
                s.persist(new Book("Beta", 300, ann));
                s.persist(new Book("Gamma", null, bob));
                s.getTransaction().commit();
            }
            try (Session s = db.sessionFactory.openSession()) {
                List<String> named = s.createSelectionQuery("select b.title from Book b where b.pages > :min order by b.title", String.class)
                        .setParameter("min", 150).getResultList();
                t.line("named parameter           : " + named);
                assertThat(named).containsExactly("Beta");

                List<String> ordinal = s.createSelectionQuery("select b.title from Book b where b.pages > ?1 order by b.title", String.class)
                        .setParameter(1, 50).getResultList();
                t.line("?1 parameter              : " + ordinal);
                assertThat(ordinal).containsExactly("Alpha", "Beta");

                List<String> page = s.createSelectionQuery("select b.title from Book b order by b.title limit 2 offset 1", String.class).getResultList();
                t.line("limit 2 offset 1 in HQL   : " + page);
                assertThat(page).containsExactly("Beta", "Gamma");

                List<String> nulls = s.createSelectionQuery("select b.title from Book b order by b.pages desc nulls last", String.class).getResultList();
                t.line("order by ... nulls last   : " + nulls);
                assertThat(nulls).containsExactly("Beta", "Alpha", "Gamma");

                Book none = s.createSelectionQuery("from Book b where b.title = 'Nope'", Book.class).getSingleResultOrNull();
                t.line("getSingleResultOrNull()   : " + none);
                assertThat(none).isNull();

                List<Book> fetched = s.createSelectionQuery("select b from Book b join fetch b.author where b.title = 'Alpha'", Book.class).getResultList();
                t.line("join fetch                : author initialized = " + Hibernate.isInitialized(fetched.get(0).author));
                assertThat(Hibernate.isInitialized(fetched.get(0).author)).isTrue();

                Long count = s.createSelectionQuery("select count(b) from Book b where b.author.name = :n", Long.class).setParameter("n", "Ann").getSingleResult();
                t.line("count over an association : " + count);
                assertThat(count).isEqualTo(2L);
            }
            try (Session s = db.sessionFactory.openSession()) {
                s.beginTransaction();
                int updated = s.createMutationQuery("update Book b set b.pages = b.pages + 1 where b.pages is not null").executeUpdate();
                s.getTransaction().commit();
                t.line("createMutationQuery update: " + updated + " rows");
                assertThat(updated).isEqualTo(2);
            }
        }
    }

    @Test
    void statelessSession() {
        try (Db db = new Db(Author.class); Transcript t = new Transcript("12-cheat-stateless-session")) {
            try (StatelessSession s = db.sessionFactory.openStatelessSession()) {
                s.beginTransaction();
                for (int i = 1; i <= 3; i++) {
                    s.insert(new Author("user" + i + "@example.com", "User " + i));
                }
                s.getTransaction().commit();
                t.line("insert x3 (no persistence context) done");
            }
            try (StatelessSession s = db.sessionFactory.openStatelessSession()) {
                Author a1 = s.get(Author.class, 1L);
                Author a1again = s.get(Author.class, 1L);
                t.line("get twice -> same instance: " + (a1 == a1again) + " (a stateless session has no first-level cache)");
                assertThat(a1).isNotNull();
                assertThat(a1).isNotSameAs(a1again);
                t.line("rows in table: " + db.jdbc("select count(*) from Author"));
                assertThat(db.jdbc("select count(*) from Author")).containsExactly("3");
            }
        }
    }

    @Test
    void naturalId() {
        try (Db db = new Db(Author.class); Transcript t = new Transcript("13-cheat-natural-id")) {
            try (Session s = db.sessionFactory.openSession()) {
                s.beginTransaction();
                s.persist(new Author("[email protected]", "Ann"));
                s.getTransaction().commit();
            }
            try (Session s = db.sessionFactory.openSession()) {
                db.clear();
                Author a = s.bySimpleNaturalId(Author.class).load("[email protected]");
                t.line("bySimpleNaturalId(...).load -> " + a.name);
                t.line("sql: " + db.sql.get(0));
                assertThat(a.name).isEqualTo("Ann");
                assertThat(db.sql.get(0)).contains("email");
            }
        }
    }

    @Test
    void softDelete() {
        try (Db db = new Db(Note.class); Transcript t = new Transcript("14-cheat-soft-delete")) {
            Long id;
            try (Session s = db.sessionFactory.openSession()) {
                s.beginTransaction();
                Note n = new Note("remember");
                s.persist(n);
                id = n.id;
                s.getTransaction().commit();
            }
            try (Session s = db.sessionFactory.openSession()) {
                s.beginTransaction();
                db.clear();
                s.remove(s.find(Note.class, id));
                s.getTransaction().commit();
                t.line("remove(note) ran: " + db.sql.get(db.sql.size() - 1));
                assertThat(db.sql.get(db.sql.size() - 1)).startsWith("update");
            }
            try (Session s = db.sessionFactory.openSession()) {
                t.line("find afterwards       : " + s.find(Note.class, id));
                t.line("row still in the table: " + db.jdbc("select * from Note"));
                assertThat(s.find(Note.class, id)).isNull();
                assertThat(db.jdbc("select count(*) from Note")).containsExactly("1");
            }
        }
    }

    @Test
    void recordEmbeddable() {
        try (Db db = new Db(Shop.class); Transcript t = new Transcript("15-cheat-record-embeddable")) {
            Long id;
            try (Session s = db.sessionFactory.openSession()) {
                s.beginTransaction();
                Shop shop = new Shop("Corner", new Address("1 Main St", "Pune"));
                s.persist(shop);
                id = shop.id;
                s.getTransaction().commit();
            }
            try (Session s = db.sessionFactory.openSession()) {
                Shop shop = s.find(Shop.class, id);
                t.line("loaded: " + shop.address);
                t.line("columns: " + db.jdbc("select column_name from information_schema.columns where table_name = 'SHOP' order by column_name"));
                assertThat(shop.address).isEqualTo(new Address("1 Main St", "Pune"));
            }
        }
    }
}

Source: CheatSheetTest.java.

Session API: persist, find, getReference, merge, findMultiple, remove
persist(a)               -> id assigned on the object: 1
find(Author, id)         -> Ann  (1 select)
getReference(Author, id) -> initialized: false  (0 selects)
merge(detached)          -> returns a different instance: true, name now Ann Smith
findMultiple(ids 3)      -> 3 entries, missing id gives: null
remove(a)                -> find afterwards: null

Output: 10-cheat-session-api.txt. persist puts the id on the object you passed in. find runs one select. getReference in a fresh session runs none and returns an uninitialized proxy. merge returns a different, managed instance. findMultiple returns null in the slot of an id that does not exist.

HQL: parameters, limit and offset, nulls, mutation queries
named parameter           : [Beta]
?1 parameter              : [Alpha, Beta]
limit 2 offset 1 in HQL   : [Beta, Gamma]
order by ... nulls last   : [Beta, Alpha, Gamma]
getSingleResultOrNull()   : null
join fetch                : author initialized = true
count over an association : 2
createMutationQuery update: 2 rows

Output: 11-cheat-hql.txt. Selections go through createSelectionQuery(hql, Class) and updates through createMutationQuery(hql). limit, offset and nulls last are written in the HQL itself. getSingleResultOrNull() returns null instead of throwing.

StatelessSession: insert and get
insert x3 (no persistence context) done
get twice -> same instance: false (a stateless session has no first-level cache)
rows in table: [3]

Output: 12-cheat-stateless-session.txt. A stateless session has no first-level cache, so two get calls return two different instances.

Natural id lookup
bySimpleNaturalId(...).load -> Ann
sql: select a1_0.id,a1_0.email,a1_0.name from Author a1_0 where a1_0.email=?

Output: 13-cheat-natural-id.txt. bySimpleNaturalId(...).load(...) generates a select on the natural id column.

@SoftDelete
remove(note) ran: update Note set deleted=true where id=? and deleted=false
find afterwards       : null
row still in the table: [TRUE 1 remember]

Output: 14-cheat-soft-delete.txt. remove becomes an update of a deleted flag; the row stays in the table and find no longer returns it.

A Java record as an embeddable
loaded: Address[street=1 Main St, city=Pune]
columns: [CITY, ID, NAME, STREET]

Output: 15-cheat-record-embeddable.txt. The record’s components become columns of the owning table, and the loaded value equals the original.

I did not run these snippets on Hibernate 5, so the cheat sheet makes no claim about what 5.6.15 could or could not do. It records what works on 7.4.14.

The upgrade, in order

Show the upgrade checklist
  1. On 5.6.15, replace save, update, saveOrUpdate, delete, load, createSQLQuery and setFlushMode with the calls that compile on both versions.
  2. Replace Criteria and org.hibernate.criterion with HQL.
  3. Change the Maven coordinates to org.hibernate.orm:hibernate-core and every javax.persistence import to jakarta.persistence.
  4. Replace @Where with @SQLRestriction, and look at every @Type(type = ...) and @GenericGenerator(strategy = ...).
  5. Run schema validation against a copy of your real database and decide, column by column, whether to keep the old type or migrate it.
  6. Check the id sequences: names and increments differ by default.
  7. Run the tests and read the SQL.

Frequently asked questions

Can Hibernate 7 read javax.persistence annotations?

No. In the test here a class annotated with javax.persistence.Entity compiled against 5.6.15 and failed against 7.4.14, and the jakarta.persistence version did the reverse. Every import has to change.

Does session.save() still exist in Hibernate 7?

No. In the test here save, update, saveOrUpdate and delete no longer compile against 7.4.14. persist, merge and remove compiled on both 5.6.15 and 7.4.14, so you can switch before upgrading.

Will my existing Hibernate 5 database pass schema validation on Hibernate 7?

Not in the test here. Hibernate 7.4.14 with hbm2ddl validate refused a schema created by 5.6.15 for the same entity, reporting a bigint column where it expected numeric(21,0). Id sequences, enum, UUID, Instant and Duration columns all had different defaults on H2.

Is the Criteria API from Hibernate 5 still available?

The old org.hibernate.Criteria and org.hibernate.criterion package are not: they did not compile against 7.4.14. I did not test the JPA Criteria API; an HQL query is the replacement that I verified.

Run it yourself

Clone the repository, go into migration-5-to-7 and run ./scripts/run-all.sh (JDK 17 or newer, Maven 3.9, Python 3, internet access for Maven Central). It runs the tests on both versions, compiles the sources in src/compat against both, and rewrites everything in output/. Build files: pom.xml and pom.xml.

Conclusion

The compiler finds most of a Hibernate 5 to 7 migration: the namespace, the removed session methods, the old Criteria API and the changed annotation attributes. Two things it cannot find are what you must plan for: the column types and id sequences Hibernate 7 generates by default, and the schema validation that will tell you about them at startup.

See also

Further reading

No Comments yet!

Leave a Reply

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