Hibernate (JPA & ORM)
|
This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases. This section’s bibliography lists the reference material consulted while preparing these pages. |
Hibernate ORM is the object/relational mapping engine that, as the default Jakarta Persistence (JPA)
provider, backs spring-boot-starter-data-jpa. This page is the conceptual overview: what problem an ORM
solves, how Hibernate’s runtime model and entity lifecycle work, where lazy loading and caching bite, and how
Spring Boot configures it. A dedicated in-depth Hibernate guide is available at
Hibernate Reference.
For the repository-level API built on top of this — JpaRepository, derived query methods, @Query,
Specifications, projections, @Transactional patterns, auditing, JdbcClient — see
Spring Data JPA and
Spring Data Overview. This page stays one layer below that.
The object/relational paradigm mismatch
An object graph in memory and a set of relational tables are two different data models, and mapping between them is not mechanical. The friction points, collectively the object/relational paradigm mismatch:
-
Granularity — classes come in many sizes (an
Addressvalue object, aMoneytype), but a schema tends toward a coarser grain of tables and columns. A fine-grained class often maps to a few columns of another table rather than a table of its own. -
Subtypes and inheritance — Java has class hierarchies and polymorphism; SQL has neither. An
Employee/Managerhierarchy has to be flattened onto tables somehow (one table, a table per subclass, a table per concrete class). -
Identity — a row is identified by its primary key; a Java object has both reference identity (
==) and anequals-defined equality. "Same row" and "same object" and "equal objects" are three distinct notions that Hibernate has to keep aligned within a unit of work. -
Associations — an object reference is directional and single-ended (
order.getCustomer()); a foreign key is undirected and is navigated by joining in either direction. Many-to-many needs an explicit link table with no object counterpart. -
Data navigation — object code walks a graph one reference at a time (
order.getCustomer().getAddress()); doing that against a database means either many small `SELECT`s or one well-chosen join. The natural access pattern on each side is the wrong one for the other.
Hibernate/JPA is the mapping layer that bridges this: you annotate classes to declare the correspondence, and the provider generates SQL, manages identity, and translates graph navigation into queries.
Core runtime model
Two objects, at two very different lifetimes:
-
An
EntityManagerFactory(JPA) —SessionFactoryin Hibernate’s native API — is the heavyweight, thread-safe, application-scoped bootstrap object. It holds the parsed mapping metadata, the connection pool reference, the second-level cache, and compiled query plans. It is expensive to build, so an application builds exactly one per persistence unit and keeps it for its whole lifetime. Under Spring Boot it is a singleton bean. -
An
EntityManager(JPA) —Sessionnatively — is the lightweight, not thread-safe, short-lived handle for a single unit of work (typically one transaction / one request). It wraps a JDBC connection, tracks the entities you have touched, and is discarded at the end of the unit of work. -
The persistence context is the set of entity instances an
EntityManageris currently managing. It is an identity map — ask for the same primary key twice in one context and you get back the very same object instance — and it is the first-level cache: afindfor an id already in the context returns without hitting the database.
Entity lifecycle
Every entity instance is in one of four states with respect to a persistence context:
-
transient (new) — a plain
newobject, no persistent identity, unknown to any context. -
managed (persistent) — associated with a context, has a database identity; changes to it are tracked and will be synchronised to the database.
-
detached — was managed, but its context has closed (or it was explicitly evicted); it still has an identity but its changes are no longer tracked.
-
removed — scheduled for deletion; still in the context until the next flush, then gone.
Two behaviours make this work without explicit "save" calls:
-
Dirty checking — at flush time Hibernate compares each managed entity against a snapshot taken when it was loaded and issues
UPDATEonly for the ones that actually changed. You mutate a managed entity with plain setters; no repository call is needed for the update to happen. -
Automatic flush — the persistence context is flushed (pending SQL sent to the database, still inside the transaction) automatically before the transaction commits and, by default, before running a query whose results could be affected by pending changes.
FlushModeType.COMMITrelaxes the second case. Flush is not commit — it writes SQL; the transaction still decides whether it sticks.
merge is the operation people trip over: it does not attach the instance you pass. It copies that
instance’s state onto a managed instance (loading one if necessary) and returns that managed instance — the
argument stays detached.
Mapping essentials
A quick tour of the annotations (from jakarta.persistence.*); the worked, runnable mapping examples live in
Spring Data JPA:
-
@Entityon the class,@Tableto override the table name. -
@Idmarks the primary-key field;@GeneratedValue(strategy = …)delegates key generation to the database (IDENTITY,SEQUENCE) or to Hibernate. -
@Embeddable/@Embeddedmap a fine-grained value type (anAddress) into columns of the owning entity’s table — the granularity mismatch, handled. -
@ManyToOneis the owning side of a to-one association and maps to a foreign-key column (@JoinColumn).@OneToMany(mappedBy = "…")is the inverse, non-owning collection side.@ManyToManymaps through a@JoinTable.fetch = FetchType.LAZY/EAGERsets when the association is loaded (see below). -
Inheritance, one line each:
SINGLE_TABLE— whole hierarchy in one table with a discriminator column (fast, but subclass columns must be nullable);JOINED— a table per class, joined on the shared key (normalised, join cost per query);TABLE_PER_CLASS— a table per concrete class with all columns repeated (no joins, but polymorphic queries need aUNION).
Fetching, caching, and the N+1 problem
Lazy vs. eager. A lazy association is fetched only when first accessed, via a proxy; an eager one is fetched
with its owner. @ManyToOne/@OneToOne default to eager, @OneToMany/@ManyToMany to lazy. Prefer lazy
almost everywhere and fetch what a given use case needs explicitly.
The N+1 problem. Load 100 orders with one query, then loop and touch order.getCustomer() on each: if that
association is lazy and not already cached, Hibernate issues one SELECT per order — 1 + 100 queries for what
should be one or two. The usual fixes:
-
a
JOIN FETCHin the JPQL/HQL query (SELECT o FROM Order o JOIN FETCH o.customer); -
a JPA entity graph (
@EntityGraph/EntityManagerhints) that declares which associations to load eagerly for this query only; -
@BatchSize/hibernate.default_batch_fetch_size, which turns the N follow-up selects intoSELECT … WHERE id IN (?, ?, …)batches.
Caching. The first-level cache is the persistence context itself — always on, scoped to one
EntityManager, not shared. The second-level cache is optional, shared across EntityManager`s at the
`EntityManagerFactory level, and holds entity/collection state (and, separately, a query cache) between
transactions; it needs a provider (Ehcache, Infinispan, Caffeine, …) and per-entity @Cache opt-in. See
Caching for how it relates to Spring’s own cache abstraction.
Querying options (conceptual)
Three ways to express a query against mapped entities; syntax and worked examples are in Spring Data JPA and the Hibernate User Guide:
-
JPQL / HQL — an object-oriented query language over entity names and fields rather than tables and columns. HQL is Hibernate’s superset of the JPA-standard JPQL. It is concise and portable across databases, and is the default choice for static queries. Written as strings, so it is only checked at startup (or at runtime).
-
Criteria API — a typed, programmatic query builder. Verbose, but the query is built from Java objects, so it composes well when the shape of the query depends on runtime conditions (optional filters, dynamic sorting) and, with the generated static metamodel, is refactor-safe.
-
Native SQL — a raw SQL string, with results mapped back to entities or to a
@SqlResultSetMapping/ projection. The escape hatch for vendor-specific features (window functions, hints, recursive CTEs) that JPQL cannot express. Ties the query to one database dialect.
Schema generation vs. migrations
Hibernate can generate DDL from the mappings. spring.jpa.hibernate.ddl-auto selects the strategy: none
(do nothing), validate (check the existing schema matches the mappings and fail fast if not), update
(attempt to alter the schema to fit — never drops anything, easily drifts), create / create-drop (drop
and recreate on startup / also on shutdown).
update and create-drop are development conveniences only. Real schema change — in staging and
production, reviewed, ordered, reversible, recorded — belongs to a migration tool
(Evolving the Database Model covers Liquibase, Mongock and
Flamingock). The production-safe combination is a migration tool owning the schema plus
spring.jpa.hibernate.ddl-auto=validate so a mapping that has drifted from the migrated schema fails the
application at startup instead of at the first query.
Spring Boot integration
spring-boot-starter-data-jpa pulls in Hibernate ORM as the default provider, Jakarta Persistence, Spring ORM,
and a connection pool (HikariCP), and auto-configures an EntityManagerFactory from spring.datasource. and
spring.jpa.. HibernateJpaVendorAdapter is the adapter Boot uses to plug Hibernate in as that provider and
to translate common settings (dialect detection, DDL mode) into Hibernate properties. Anything not exposed as a
first-class property is set through spring.jpa.properties.hibernate.* (for example
spring.jpa.properties.hibernate.jdbc.batch_size).
spring.jpa.open-in-view defaults to true: it keeps the persistence context open for the whole HTTP request,
so lazy associations still resolve while the view/serializer runs. It is convenient but hides N+1 problems in
the web layer, holds a database connection for longer than the service method, and blurs the transaction
boundary. Set spring.jpa.open-in-view=false and fetch exactly what each response needs inside the
transactional service method.
Use a Spring Data JpaRepository for the common cases (CRUD, derived finders, paging). Inject a raw
EntityManager (with @PersistenceContext or constructor injection) when you need provider-level control:
createEntityGraph, flush / clear in a batch loop, getReference for a foreign-key-only association,
stateless bulk operations, or a Criteria query built by hand.
@Entity
@Table(name = "book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String isbn;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
// getters and setters omitted
}
@Repository
public class BookQueryRepository {
@PersistenceContext
private EntityManager em;
// JPQL with an explicit join fetch -- one query, no N+1 on author
public List<Book> findByAuthorName(String name) {
return em.createQuery("""
SELECT b FROM Book b
JOIN FETCH b.author a
WHERE a.name = :name
""", Book.class)
.setParameter("name", name)
.getResultList();
}
@Transactional
public void add(Book book) {
em.persist(book); // transient -> managed; INSERT flushed at commit
}
}
spring:
datasource:
url: jdbc:postgresql://localhost:5432/library
username: library
password: library
jpa:
open-in-view: false
show-sql: true
hibernate:
ddl-auto: validate # a migration tool owns the schema
properties:
hibernate:
format_sql: true
jdbc:
batch_size: 50
default_batch_fetch_size: 16
For full-text search over entity data, Hibernate Search indexes mapped entities into a Lucene-based backend (Elasticsearch or OpenSearch) and keeps the index in sync with ORM writes, so you can run text queries with a Hibernate-style API against the same domain model. It is a separate dependency; see Elasticsearch for the search engine it sits on and the Hibernate Search documentation.