The Spring Boot 2 to 3 migration guides all mention it in passing: “Boot 3 ships Hibernate 6.” What they undersell is that Hibernate 6 is the part of the upgrade most likely to change your application’s runtime behavior without a compile error. The jakarta namespace change breaks loudly. Hibernate 6 breaks quietly, in ID generation, timezone handling, and SQL generation.
This is the checklist I work through on every migration, built from doing this on production systems where the database is the part you really don’t want to surprise.
Dialects: Delete Version-Specific Ones
Hibernate 5 had a dialect class per database version: MySQL57Dialect, PostgreSQL10Dialect, and so on. Hibernate 6 consolidated these into one dialect per database that adapts based on the version it detects at runtime.
# Before (Hibernate 5)
spring:
jpa:
properties:
hibernate.dialect: org.hibernate.dialect.PostgreSQL10Dialect
# After (Hibernate 6): best option is no dialect property at all
spring:
jpa:
properties: {}
The version-specific classes are deprecated or removed, and some fail at startup. The best move is deleting the property entirely and letting Hibernate detect dialect and version from JDBC metadata. Only keep an explicit dialect if your startup environment can’t reach the database (and then use the base class, like org.hibernate.dialect.PostgreSQLDialect).
ID Generation: Verify Before You Trust It
This is the highest-stakes change on the list because getting it wrong corrupts data. Hibernate 6 changed defaults around sequence-based ID generation:
- Implicit sequence naming changed. An entity with
@GeneratedValue(strategy = SEQUENCE)and no explicit generator may now look for a differently-named sequence (<table>_seqper-entity rather than a sharedhibernate_sequencein older configurations). - The interaction between
allocationSizeand the database sequence’sINCREMENT BYis enforced differently by the pooled optimizer.
Audit every entity that doesn’t spell out its generator, and make the mapping explicit:
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
@SequenceGenerator(name = "order_seq", sequenceName = "order_id_seq", allocationSize = 50)
private Long id;
Then verify against the actual schema:
-- allocationSize = 50 requires INCREMENT BY 50 on the database side
SELECT sequencename, increment_by FROM pg_sequences WHERE sequencename = 'order_id_seq';
If the allocationSize says 50 and the sequence increments by 1, you get either duplicate key violations under load or IDs jumping by thousands. Both have shown up in real migrations; the duplicates are the one that pages you at night.
Timezone Storage
Hibernate 6 changed how timezone-aware types are stored. The new hibernate.timezone.default_storage setting defaults to NORMALIZE, which normalizes OffsetDateTime and ZonedDateTime values to the JVM’s timezone before storing them in a timestamp column.
If your application servers don’t run in UTC, or if Hibernate 5 was writing these columns differently, the same code can now read back different instants. The safe, boring configuration:
- Run the JVM in UTC (
-Duser.timezone=UTC), which you probably wanted anyway. - Or set the JDBC time zone explicitly:
spring:
jpa:
properties:
hibernate.jdbc.time_zone: UTC
Then write a round-trip test: persist a known OffsetDateTime in a non-UTC offset, read it back, and assert the instant is identical. Run it against your production database engine, not H2, because this behavior is exactly the kind of thing embedded databases fake differently.
The @Type Annotation Rewrite
Hibernate 5’s string-based type mappings are gone:
// Before (Hibernate 5)
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private Map<String, Object> attributes;
// After (Hibernate 6)
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
private Map<String, Object> attributes;
For JSON columns, Hibernate 6 has native support via @JdbcTypeCode(SqlTypes.JSON), which often means you can delete the third-party hibernate-types library (or upgrade to a Hibernate 6-compatible version of it). Custom UserType implementations need rewriting against the new interface, which changed from string keys to typed class references. Enums mostly get simpler: @Enumerated(EnumType.STRING) keeps working, and @JdbcTypeCode covers the exotic cases.
Stricter Query Semantics
Hibernate 6 replaced its query translator with the Semantic Query Model, and the new one is a stricter grader. Patterns that ran on Hibernate 5 and now fail or behave differently:
- Implicit type mismatches. Comparing a numeric parameter to a string column (or vice versa) now fails validation instead of silently casting.
- Positional parameter quirks and legacy HQL syntax that the old parser tolerated.
- Tuple and aggregation return types. Some queries that returned
Object[]or loosely-typed results now return different types; code that casts query results is worth auditing. - Fetch behavior in pagination. The classic “firstResult/maxResults with collection fetch” combination now behaves differently (and warns loudly), because in-memory pagination of joined collections was a footgun that version 6 stopped hiding.
The good news is that most of these fail fast at startup if you enable query validation in a test that loads all named queries and repositories. A full integration test suite over your repository layer, per my Spring Data JPA best practices, catches nearly all of it before production does.
Inspect the SQL Diff
The most effective single technique for this migration: capture the SQL before and after. Turn on SQL logging in a test profile, run your integration suite on Hibernate 5, save the log, then run the same suite after the upgrade and diff the statements.
spring:
jpa:
properties:
hibernate.format_sql: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE # parameter binding in Hibernate 6
You’re looking for changed join strategies, new subqueries, different sequence access patterns, and pagination changes. Most diffs are harmless; the ones that aren’t are far cheaper to find in a log diff than in a production latency graph.
The Short Checklist
For teams that want the compressed version:
- Remove version-specific dialect properties.
- Make every sequence generator explicit; verify
allocationSizeagainst the database. - Pin timezone behavior (UTC JVM or
hibernate.jdbc.time_zone) and round-trip test it. - Rewrite
@Typeusages and customUserTypes; adopt native JSON support. - Run the full repository test suite against the real database engine via Testcontainers.
- Diff the generated SQL before and after.
Hibernate 6 is genuinely better than 5: faster, stricter, and with native support for things that used to need third-party libraries. But it earns that by changing behavior, and behavior changes in the persistence layer deserve more respect than a version bump in a pom file.
This migration rarely happens alone; it arrives bundled with the Spring Boot 2 to 3 upgrade and the jakarta namespace change. If your data layer is large, old, or load-bearing enough that surprises are unacceptable, that’s the work I specialize in. Read about my Java modernization services or book a strategy call.
Java Modernization Readiness Assessment
15 questions your team should answer before starting a migration. Takes 10 minutes. Could save you months.