JPA and Hibernate Fundamentals - Complete Notes

A complete learning and interview guide to object-relational mapping, Jakarta Persistence, Hibernate, entity modeling, persistence contexts, lifecycle operations, relationships, locking, queries, PostgreSQL integration, performance, security, testing, and production practices.

00. The ORM mental model

An ORM maps an in-memory object graph to relational rows, but the object model and the relational model remain different systems with different rules.

Java code works with identity, references, inheritance, collections, and mutable objects. A relational database works with tables, keys, constraints, joins, sets of rows, and transactions. Object-relational mapping, or ORM, records how those concepts correspond and coordinates SQL for a unit of work.

Think of the persistence context as a careful translator with a notebook. It remembers which database row corresponds to each Java object, tracks changes made to managed objects, and writes the required SQL when synchronization is necessary. The translator reduces repetitive work, but it cannot choose good table design, transaction boundaries, indexes, or fetch plans for you.

ORM does not remove SQL

Every useful mapping eventually becomes SQL. Correct ORM code requires understanding the generated statements, database constraints, query plans, locks, and transaction behavior.

Object concept Relational concept Important mismatch
Object identity Primary key Java reference equality is not database identity
Reference Foreign key and join Following a reference can require a query
Collection Multiple rows or join table Ordering and uniqueness must be mapped explicitly
Inheritance No direct equivalent Every strategy has storage and query costs
Field update UPDATE SQL may be delayed until flush
Null reference NULL Database null has three-valued logic

01. Jakarta Persistence, Hibernate, and Spring

Jakarta Persistence defines the portable contract. Hibernate ORM implements that contract. Spring integrates the provider with dependency injection, transactions, configuration, and exception translation.

Layer What it provides Typical API
Jakarta Persistence Standard annotations, entity lifecycle, persistence context, query and locking APIs EntityManager, @Entity, JPQL
Hibernate ORM JPA provider, mapping engine, SQL generation, dirty checking, provider extensions Session, HQL, Hibernate annotations
Spring Framework Entity manager integration, transaction management, resource binding, exceptions @Transactional, JpaTransactionManager
Spring Boot Conditional setup of the data source, entity manager factory, provider, and settings spring.jpa.*, starters
Spring Data JPA Repository abstraction built on JPA JpaRepository, specifications

Code written only to standard Jakarta Persistence APIs is easier to move between providers. Hibernate-specific features can still be valuable when they solve a measured problem. Isolate provider extensions and document the portability tradeoff.

In modern code the package is jakarta.persistence, not the older javax.persistence. A package migration is a source-level compatibility change, so library versions must be aligned through the Spring Boot dependency platform.

02. Spring Boot and PostgreSQL setup

The JPA starter assembles Spring Data JPA, Spring ORM, and a supported Hibernate version. The PostgreSQL JDBC driver remains a runtime dependency.

Minimal Maven dependencies
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>
Production-oriented starting configuration
spring.datasource.url=jdbc:postgresql://db.internal:5432/orders
spring.datasource.username=orders_app
spring.datasource.password=${ORDERS_DB_PASSWORD}

spring.jpa.hibernate.ddl-auto=validate
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.jdbc.time_zone=UTC

Boot scans its auto-configuration packages for @Entity, @Embeddable, and @MappedSuperclass types. Put the application class above domain packages or configure scanning deliberately. Boot normally creates a LocalContainerEntityManagerFactoryBean and a transaction manager when the required classes, data source, and conditions are present.

Do not use automatic schema mutation as a production migration system

ddl-auto=update is convenient for experiments but cannot express reviewed, repeatable, zero-downtime migrations reliably. Use Flyway or Liquibase, make migration files the schema authority, and use validate to detect mapping drift.

Factory and manager roles

EntityManagerFactory is expensive to build and is thread-safe. It owns mapping metadata and creates entity managers. An EntityManager represents one persistence context and is not thread-safe. Spring normally injects a shared proxy that delegates to the real transaction-bound entity manager for the current thread.

03. Entities and mapping basics

An entity has persistent identity. Its class describes how state maps to a table and its instances participate in a managed lifecycle.

A domain-focused entity
@Entity
@Table(
    name = "purchase_order",
    uniqueConstraints = @UniqueConstraint(
        name = "uk_purchase_order_number",
        columnNames = "order_number"
    )
)
public class PurchaseOrder {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
    @SequenceGenerator(
        name = "order_seq",
        sequenceName = "purchase_order_id_seq",
        allocationSize = 50
    )
    private Long id;

    @Version
    private long version;

    @Column(name = "order_number", nullable = false, updatable = false, length = 40)
    private String orderNumber;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 24)
    private OrderStatus status;

    @Embedded
    private Money total;

    protected PurchaseOrder() {
        // Required for provider construction.
    }

    public PurchaseOrder(String orderNumber, Money total) {
        this.orderNumber = Objects.requireNonNull(orderNumber);
        this.total = Objects.requireNonNull(total);
        this.status = OrderStatus.PENDING;
    }

    public void confirm() {
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException("Only pending orders can be confirmed");
        }
        status = OrderStatus.CONFIRMED;
    }
}

A portable entity is a top-level class or static nested class, is not final, and has a public or protected no-argument constructor. Hibernate can support some additional shapes, but proxying and portability are simplest when the standard requirements are followed. An entity itself cannot be a Java record, although Jakarta Persistence 3.2 supports records as embeddable types.

Field access versus property access

Placing @Id on a field selects field access by default. Placing it on a getter selects property access. With field access, Hibernate reads and writes fields directly and business methods can stay expressive. With property access, persistence goes through getters and setters. Do not mix access styles accidentally. Use @Access when a deliberate override is needed.

Map important database rules explicitly

  • Use stable table and column names rather than relying entirely on naming conventions.
  • Mirror required values with database NOT NULL constraints.
  • Enforce uniqueness in the database, not only with an application lookup.
  • Choose adequate precision and scale for decimal values.
  • Use Bean Validation for helpful input feedback and database constraints for final integrity.
  • Keep entities behavior-rich enough to protect local invariants.

04. Identifiers and entity equality

Every entity needs a stable identifier. Identifier strategy affects insert timing, batching, portability, equality, and database operations.

Strategy Behavior Practical note
SEQUENCE Obtains values from a database sequence Strong PostgreSQL choice and supports allocation
IDENTITY Database generates during insert Can force earlier inserts and restrict insert batching
TABLE Uses a table as an identifier allocator Portable but usually adds contention and overhead
AUTO Provider chooses based on type and database Convenient, but less explicit
Assigned UUID Application or provider assigns before insert Useful across nodes, with larger indexes than integers

For sequence allocation, the mapping's allocationSize and the database sequence increment must agree. Pooled allocation reduces sequence calls. Gaps are normal after rollbacks, crashes, caching, or pooled allocation, so identifiers must not be treated as gapless business numbers.

Composite keys use @EmbeddedId or @IdClass. The key class must model database equality correctly and implement equals and hashCode. Composite foreign keys spread through other tables and APIs, so use them when the domain truly needs them rather than as a default.

Choosing equals and hashCode

Default reference equality is safe inside one persistence context because only one managed object represents a given entity identity there. It becomes awkward across detached instances. Equality based only on a generated identifier is also tricky because the identifier may be null before persistence and changing a hash code while an object is inside a HashSet breaks the collection.

  • Use an immutable, truly unique business key when the domain has one.
  • Otherwise use a carefully documented generated-id pattern with a stable hash strategy.
  • Do not include mutable fields or lazy associations in equality methods.
  • Make equality proxy-safe if Hibernate proxies can appear.
  • Test transient, managed, detached, and proxy cases before using entities in sets or map keys.

05. Entity lifecycle states

Lifecycle state describes an object's relationship to one persistence context, not whether a row happens to exist in the database.

State Meaning How it changes
New or transient No persistent identity and not managed persist can make it managed
Managed or persistent Associated with the current persistence context Loaded, persisted, or returned from merge
Detached Has identity but is no longer associated Context closes, clears, or explicitly detaches it
Removed Managed and scheduled for deletion remove marks it until flush
Lifecycle transitions in one transaction
@Transactional
public void lifecycle(EntityManager em, Long id) {
    PurchaseOrder created = new PurchaseOrder("PO-1042", Money.usd("30.00"));
    // created is new

    em.persist(created);
    // created is managed; INSERT may still be deferred

    PurchaseOrder found = em.find(PurchaseOrder.class, id);
    // found is managed if a row exists

    em.detach(found);
    // found is detached; later changes are not tracked

    PurchaseOrder managedCopy = em.merge(found);
    // managedCopy is managed; found remains detached

    em.remove(managedCopy);
    // managedCopy is removed; DELETE may still be deferred
}

EntityManager.contains(entity) can test whether a particular instance is managed. An object with a non-null identifier is not automatically detached. Assigned identifiers, manually constructed objects, and deleted rows make that shortcut unreliable.

06. Persistence context and first-level cache

A persistence context is an identity map and unit of work. For one entity type and identifier, it contains at most one managed Java instance.

Identity guarantee inside one context
PurchaseOrder first = em.find(PurchaseOrder.class, 42L);
PurchaseOrder second = em.find(PurchaseOrder.class, 42L);

assert first == second;

The second lookup can be satisfied from the first-level cache. This cache is mandatory and local to the persistence context. It is not a cross-request cache and it does not mean every query is skipped. A JPQL query may still execute SQL, then resolve matching identities to managed instances.

Spring's normal transaction-scoped context begins with a transaction and closes when the transaction ends. An extended context survives multiple transactions but is a poor fit for singleton services because it is not thread-safe, retains state for a long time, and can return stale data.

Never share an EntityManager across threads

A persistence context contains mutable identity, snapshots, queued actions, proxies, and JDBC coordination. Async work, parallel streams, and manually created threads need independent transactions and persistence contexts.

Context control operations

  • detach(entity) stops tracking one managed instance.
  • clear() detaches every entity and discards queued context state.
  • refresh(entity) reloads database state and overwrites unflushed changes.
  • close() ends an application-managed entity manager.
  • unwrap(Session.class) provides documented Hibernate-specific operations.

07. Dirty checking, flushing, and SQL timing

Hibernate tracks managed state. At flush it detects changes and translates the unit of work into SQL actions.

No explicit update call is needed
@Transactional
public void confirm(Long id) {
    PurchaseOrder order = entityManager.find(PurchaseOrder.class, id);
    order.confirm();
    // Hibernate detects the changed status during flush.
}

Hibernate commonly compares current values with a loaded snapshot. Bytecode enhancement can provide more direct change tracking. Dirty checking applies only to managed entities. Changing a detached entity after a transaction ends does nothing unless its state is deliberately merged or copied into another managed object.

Flush is not commit

Flush synchronizes the persistence context with the database by executing SQL. Commit completes the transaction and makes its outcome durable and visible according to database isolation. Flushed SQL can still be rolled back. A transaction may flush:

  • At transaction commit.
  • Before a query whose result could be affected by pending changes.
  • When the application calls flush().
  • At another provider-required point, such as some identifier generation cases.

With JPA FlushModeType.AUTO, the provider ensures relevant queries do not ignore pending changes. COMMIT reduces automatic flushing before queries but query results in the presence of pending changes are not generally safe to assume. Hibernate has additional native flush modes.

Why explicit flush can be useful

Flush at a deliberate boundary when code must catch a constraint or optimistic-lock failure before proceeding. After a persistence exception, treat the transaction as failed and roll it back rather than continuing with a possibly inconsistent context.

Hibernate's action queue determines SQL ordering, not the exact order of Java method calls. It groups operations to preserve constraints and enable batching. An insert can therefore execute before a delete even if remove was called first. Rely on database constraints and flush deliberately only when an intermediate order is truly required.

08. Basic values, embeddables, and converters

Value types have no independent entity identity. Their lifecycle belongs to the entity that contains them.

Mapping Use Key point
@Basic and @Column Scalar field Explicit length, precision, nullability, and mutability matter
@Enumerated Java enum Prefer STRING; ordinal values break when constants move
@Embedded Reusable value object stored in owner columns No independent identity or table is required
@ElementCollection Collection of basic or embeddable values Usually stored in a separate collection table
@Convert Application type to database representation Converter must be deterministic and preserve meaning
@Lob Large text or binary data Loading and driver behavior need measurement
Embeddable money value
@Embeddable
public record Money(
    @Column(name = "total_amount", precision = 19, scale = 2)
    BigDecimal amount,

    @Column(name = "total_currency", length = 3)
    String currency
) {
    public Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
    }
}

Use BigDecimal for exact decimal money, with a deliberate scale and rounding policy. Store instants in UTC and distinguish an instant from a local date-time that has no offset. PostgreSQL types such as jsonb, arrays, ranges, and network addresses may need Hibernate-specific mappings or custom converters. Choose them for a real query or integrity benefit, not only to avoid modeling.

09. Relationships and owning sides

A relationship mapping must express cardinality, foreign-key ownership, fetch behavior, and lifecycle rules. Cardinality alone is not enough.

Mapping Typical database shape Default fetch
@ManyToOne Foreign key on this entity's table Eager by JPA default
@OneToMany Foreign key on child or join table Lazy
@OneToOne Unique foreign key or shared primary key Eager by JPA default
@ManyToMany Join table with two foreign keys Lazy

The owning side writes the relationship columns. In a bidirectional one-to-many relationship, the many side with @ManyToOne owns the foreign key. The parent collection uses mappedBy and is the inverse side. Changing only the inverse collection changes the Java graph but may not change the foreign key.

Bidirectional parent-child mapping with helper methods
@Entity
class PurchaseOrder {
    @OneToMany(
        mappedBy = "order",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private final List<OrderLine> lines = new ArrayList<>();

    public void addLine(Product product, int quantity) {
        OrderLine line = new OrderLine(this, product, quantity);
        lines.add(line);
    }

    public void removeLine(OrderLine line) {
        lines.remove(line);
        line.detachFromOrder();
    }
}

@Entity
class OrderLine {
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "order_id", nullable = false)
    private PurchaseOrder order;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;
}

Helper methods keep both directions consistent in memory. Database foreign keys remain essential because another process, a bug, or a bulk query can bypass the object graph.

Why many-to-many often becomes an entity

A direct many-to-many is suitable only when the association has no identity, attributes, or lifecycle. Real systems soon need created_at, status, ordering, actor, quantity, or soft deletion. Model the join row as an entity with two many-to-one relationships when that happens. This also makes ownership and deletion safer.

10. Cascades and orphan removal

Cascades propagate EntityManager operations through an object graph. They are not database ON DELETE CASCADE, and they do not mean that every related entity shares a lifecycle.

Cascade Propagated operation Typical use
PERSIST New parent also persists new child Aggregate-owned children
MERGE Copies detached child state during merge Only when merge should cross the boundary
REMOVE Removing parent removes target Privately owned dependent entities
REFRESH Reload also refreshes target Rare, deliberate graph refresh
DETACH Detach also detaches target Rare context management
ALL All standard cascade operations True aggregate ownership only

orphanRemoval=true means a privately owned child removed from a one-to-one or one-to-many relationship is deleted at flush. It differs from CascadeType.REMOVE: cascade remove acts when the parent itself is removed, while orphan removal acts when the child is disconnected from the parent.

Do not cascade REMOVE through shared relationships

A product, role, tag, or customer is usually shared independently. Cascading removal from one owner can delete data still needed elsewhere. It is especially dangerous on direct many-to-many relationships.

11. Lazy loading, proxies, and fetch plans

Fetch strategy decides when data is loaded. It is a query-design concern, not a serialization convenience.

Lazy associations may be represented by proxies or persistent collection wrappers. Accessing an unloaded association asks the active persistence context to query it. If the context is closed, Hibernate cannot initialize it and may throw LazyInitializationException.

JPA defaults to eager for to-one relationships and lazy for to-many relationships. Eager is a requirement that data be available, not a guarantee of one join. Hibernate may use secondary selects, creating an N+1 query problem. Prefer lazy associations in domain mappings where practical, then select an explicit fetch plan per use case.

Fetch tool Good use Risk
JPQL join fetch Known associations needed by one query Row multiplication and pagination problems
Entity graph Reusable dynamic fetch plan Still requires SQL inspection
DTO projection Read-only API or report shape Not a managed entity graph
Batch fetching Initialize several lazy associations together Reduces but does not always eliminate extra queries
Second-level cache Stable, frequently reused shared data Invalidation, memory, and staleness complexity
Purpose-built fetch query
List<PurchaseOrder> orders = entityManager.createQuery("""
    select distinct o
    from PurchaseOrder o
    left join fetch o.lines l
    left join fetch l.product
    where o.id = :id
    """, PurchaseOrder.class)
    .setParameter("id", id)
    .getResultList();
Disable Open EntityManager in View for explicit boundaries

Keeping the context open through web rendering can hide missing fetch plans and run unexpected queries during JSON serialization. With spring.jpa.open-in-view=false, services load exactly what they need inside a transaction and map it to response DTOs.

Collection fetch joins and pagination

A collection join repeats the parent row for each child. Database limit and offset operate on SQL rows, not distinct parent entities, so applying pagination directly can produce incomplete pages or force in-memory work. Page parent identifiers first, then fetch the selected graph in a second query, or use a suitable DTO query.

12. Inheritance strategies

Relational databases do not model Java inheritance directly. JPA offers strategies with different query, constraint, and storage tradeoffs.

Strategy Schema Strength Cost
@MappedSuperclass Fields copied into each entity table Simple reuse without polymorphic entity queries Base type is not an entity
SINGLE_TABLE One table plus discriminator Fast polymorphic reads and no joins Many nullable subtype columns and weaker subtype constraints
JOINED Base table plus one table per subtype Normalized columns and stronger constraints Polymorphic reads and writes need joins
TABLE_PER_CLASS Complete columns in each concrete table No joins for concrete-type reads Polymorphic queries require unions and ids are harder

SINGLE_TABLE is the JPA default when an entity hierarchy has no explicit strategy. Choose based on actual query patterns and integrity needs. Composition is often simpler than persistence inheritance, especially when subtypes do not need polymorphic queries.

13. Optimistic locking

Optimistic locking allows concurrent readers and rejects a stale writer instead of holding a database lock for the whole think time.

Versioned entity update
@Version
@Column(nullable = false)
private long version;

Hibernate reads the version and generates an update conceptually like:

Version check in SQL
update purchase_order
set status = ?, version = version + 1
where id = ? and version = ?;

If another transaction has already changed the row, zero rows match. The provider throws an optimistic locking exception and the transaction must roll back. Detection may happen at flush or commit. Call flush() inside a try boundary if the application must detect it at a specific point.

  • Add @Version to entities that can be edited concurrently or merged when detached.
  • Return a clear conflict response, commonly HTTP 409, when a user submits stale state.
  • Retry only when the whole operation is safe and its inputs can be reevaluated.
  • Use bounded retry with jitter for short machine-driven conflicts.
  • Do not blindly retry a human edit because it can overwrite a meaningful concurrent decision.

OPTIMISTIC_FORCE_INCREMENT can advance the version even when direct fields did not change. It is useful when a logical aggregate change must conflict with another update, but it increases contention and should be applied deliberately.

14. Pessimistic locking and PostgreSQL

Pessimistic locking asks the database to lock selected rows now. It trades earlier conflict control for waiting, deadlock risk, and reduced concurrency.

JPA pessimistic write lock with timeout hint
PurchaseOrder order = entityManager.find(
    PurchaseOrder.class,
    id,
    LockModeType.PESSIMISTIC_WRITE,
    Map.of("jakarta.persistence.lock.timeout", 2_000)
);
JPA lock Intent PostgreSQL-style effect
PESSIMISTIC_READ Prevent incompatible updates while allowing suitable readers Provider selects an appropriate shared row lock
PESSIMISTIC_WRITE Reserve the row for update Commonly translates to SELECT ... FOR UPDATE
PESSIMISTIC_FORCE_INCREMENT Lock and increment a versioned entity Row lock plus version update behavior

PostgreSQL row locks block conflicting writers and lockers, not ordinary snapshot reads. Locks remain until the transaction ends. Keep locked transactions short, lock rows in a consistent order, set lock and statement timeouts, and handle deadlock or serialization failures by retrying the complete transaction when safe.

Lock timeout hints are not equally portable across providers and databases. Verify the generated SQL and deployed behavior. Features such as NOWAIT or SKIP LOCKED can be useful for work queues, but they are database-specific coordination tools and do not replace idempotency.

Never hold a database transaction while waiting for a user or remote service

The transaction retains a connection, may retain locks, delays cleanup, and increases failure cost. Persist a state transition, commit, perform the external workflow with an idempotent protocol, then use another short transaction.

15. JPQL, Criteria API, and native SQL

Choose the highest-level query language that expresses the requirement clearly, then inspect the generated SQL and database plan.

JPQL

JPQL queries entities and their mapped attributes, not raw tables and columns. It is concise, portable, supports joins and projections, and is usually the best default for stable queries.

Typed JPQL projection
List<OrderSummary> summaries = entityManager.createQuery("""
    select new com.example.orders.OrderSummary(
        o.id, o.orderNumber, o.status, o.total.amount
    )
    from PurchaseOrder o
    where o.status = :status
    order by o.id
    """, OrderSummary.class)
    .setParameter("status", OrderStatus.CONFIRMED)
    .setMaxResults(100)
    .getResultList();

Criteria API

Criteria builds a query as Java objects. It is verbose for fixed queries but useful for composable, optional filters. The static metamodel improves type safety. Keep predicate assembly in focused query code rather than spreading it through services.

Dynamic criteria query
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<PurchaseOrder> query = cb.createQuery(PurchaseOrder.class);
Root<PurchaseOrder> order = query.from(PurchaseOrder.class);

List<Predicate> filters = new ArrayList<>();
if (status != null) {
    filters.add(cb.equal(order.get("status"), status));
}
if (createdAfter != null) {
    filters.add(cb.greaterThan(order.get("createdAt"), createdAfter));
}

query.select(order)
    .where(filters.toArray(Predicate[]::new))
    .orderBy(cb.asc(order.get("id")));

List<PurchaseOrder> result = entityManager.createQuery(query)
    .setMaxResults(100)
    .getResultList();

Native SQL

Native SQL is appropriate for PostgreSQL-specific operators, recursive queries, window-heavy reports, carefully optimized joins, data-changing CTEs, or features that JPQL cannot represent. It provides full database power but reduces portability and makes entity result mappings more fragile. DTO or scalar results are often clearer.

Parameterized PostgreSQL query
List<Object[]> rows = entityManager.createNativeQuery("""
    select id, order_number
    from purchase_order
    where search_document @@ websearch_to_tsquery('english', :query)
    order by id
    limit :limit
    """)
    .setParameter("query", searchText)
    .setParameter("limit", 50)
    .getResultList();

Rules for every query style

  • Bind values as parameters. Never concatenate untrusted input into JPQL or SQL.
  • Allow-list dynamic column names or sort directions because identifiers are not value parameters.
  • Use deterministic ordering for pagination.
  • Set sensible maximum result sizes, timeouts, and transaction boundaries.
  • Use DTO projections when managed state and dirty checking are unnecessary.
  • Inspect generated SQL and run EXPLAIN (ANALYZE, BUFFERS) safely on representative data.
Bulk update and delete bypass managed entity state

JPQL bulk operations act directly in the database. They do not run normal per-entity dirty checking or synchronize already managed objects. Flush necessary changes first, then clear the persistence context or refresh affected entities. Lifecycle callbacks and cascades may not run as they do for individual operations.

16. persist, merge, save, remove, and refresh

These methods have distinct lifecycle semantics. Choosing by method name alone causes duplicate inserts, lost changes, or continued use of detached objects.

Operation Input Result Main warning
persist(entity) Normally new instance Same instance becomes managed; returns void Detached input can fail
merge(entity) New or detached state carrier Returns a managed copy Input instance remains detached
Hibernate save(entity) Historically new instance Historically returned identifier Deprecated since Hibernate 6 and absent from modern Session API
remove(entity) Managed entity Marks it removed Detached input is illegal
refresh(entity) Managed entity Reloads database values Discards unflushed changes
The important merge rule
PurchaseOrder detached = request.toEntity();
PurchaseOrder managed = entityManager.merge(detached);

// Continue with managed, not detached.
managed.confirm();

Merge locates or creates a managed target, copies merge-cascaded state from the source graph, and returns the managed target. It can load existing state and can overwrite fields when a partially populated detached object is treated as complete. For request updates, a safer pattern is to load the managed entity and call explicit domain methods with allowed fields.

Safer command-style update
@Transactional
public void renameOrder(Long id, String requestedNumber) {
    PurchaseOrder order = entityManager.find(PurchaseOrder.class, id);
    if (order == null) {
        throw new OrderNotFoundException(id);
    }
    order.rename(requestedNumber);
}

Hibernate's older save method was provider-specific, returned an identifier, and used Hibernate cascade semantics. It was deprecated in Hibernate 6 in favor of persist and is not part of the current Hibernate Session contract. Do not confuse it with Spring Data repository save, which decides between persistence and merge behavior using repository entity-new detection.

17. Transactions and service boundaries

A persistence context becomes meaningful inside a transaction that defines one atomic business operation.

Transaction at the application service boundary
@Service
public class OrderService {
    private final EntityManager entityManager;

    public OrderService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Transactional
    public OrderReceipt place(PlaceOrder command) {
        Customer customer = entityManager.find(
            Customer.class,
            command.customerId()
        );
        PurchaseOrder order = PurchaseOrder.place(customer, command.lines());
        entityManager.persist(order);
        return OrderReceipt.from(order);
    }
}

Spring binds the entity manager and JDBC connection to the transaction. On a normal successful return, the context flushes and the transaction commits. A runtime exception normally triggers rollback. Checked exception rollback rules must be configured when needed.

  • Put the boundary around one business use case, not around each DAO method.
  • Keep transactions short but complete enough to protect invariants.
  • Do not assume @Transactional works on same-object self-invocation through a proxy.
  • Do not send emails, publish non-transactional messages, or call slow APIs while holding locks.
  • Use an outbox or transactional event design for reliable external side effects.
  • Use read-only transactions as an intent and optimization hint, not as a security boundary.

Spring's injected entity manager is a proxy, so it can be kept as a singleton bean dependency even though the underlying entity manager is not thread-safe. The proxy routes each call to the transaction-bound instance. An application-created raw entity manager does not gain that behavior automatically.

18. PostgreSQL schema integrity

Object validation improves application feedback. PostgreSQL constraints remain the final concurrency-safe authority for stored data.

Schema rules that support the mapping
create sequence purchase_order_id_seq
    start with 1
    increment by 50;

create table purchase_order (
    id bigint primary key default nextval('purchase_order_id_seq'),
    version bigint not null,
    order_number varchar(40) not null,
    status varchar(24) not null,
    total_amount numeric(19, 2) not null,
    total_currency varchar(3) not null,
    constraint uk_purchase_order_number unique (order_number),
    constraint ck_purchase_order_amount check (total_amount >= 0)
);

create table order_line (
    id bigint primary key,
    order_id bigint not null references purchase_order(id),
    product_id bigint not null references product(id),
    quantity integer not null check (quantity > 0)
);

create index ix_order_line_order_id on order_line(order_id);
create index ix_order_line_product_id on order_line(product_id);

PostgreSQL creates indexes for primary keys and unique constraints. It does not automatically add an index for every referencing foreign-key column. Index foreign keys used for joins and parent updates or deletes when the workload justifies it. Keep constraints named so production errors can be diagnosed and translated consistently.

Hibernate mapping metadata such as nullable=false describes and can generate schema, but it is not a replacement for an actual constraint in a migration-managed database. Verify both directions: mappings match the schema, and the schema protects all writers.

19. Performance engineering

ORM performance is mostly about statement count, rows and columns transferred, query plans, persistence-context size, lock duration, and connection use.

Recognize N+1

One query loads N parents, then accessing an association executes one query for each parent. Development data often hides it because N is small. Count statements in integration tests, log SQL with bind values safely in non-production environments, and inspect representative endpoints.

JDBC batching

Batching groups compatible inserts or updates into fewer driver round trips. Configure hibernate.jdbc.batch_size, optionally order statements, and confirm that the identifier strategy permits batching. Batch size is not a promise that every statement is batched.

Bound persistence-context memory in a bulk import
for (int i = 0; i < commands.size(); i++) {
    entityManager.persist(toEntity(commands.get(i)));

    if (i > 0 && i % 50 == 0) {
        entityManager.flush();
        entityManager.clear();
    }
}

Flushing and clearing bounds memory because the context holds strong references and snapshots. After clear(), all prior instances are detached, so later code must not expect dirty checking. For very large streaming or bulk tasks, consider JDBC or Hibernate StatelessSession when entity lifecycle features are unnecessary.

Practical checklist

  • Fetch only data needed by the use case.
  • Prefer DTO projections for read-only list and report endpoints.
  • Avoid unbounded result lists and large offset pagination.
  • Use keyset pagination for deep, stable traversal where suitable.
  • Index predicates, joins, foreign keys, and ordering based on real plans.
  • Keep connection-pool size aligned with database capacity and request concurrency.
  • Set statement, lock, and transaction timeouts.
  • Monitor slow SQL, query count, pool saturation, lock waits, and transaction duration.
  • Use a second-level cache only after measuring repeat reads and defining invalidation.
  • Test with production-like row counts and data distribution.

20. Security and data exposure

ORM prevents some repetitive SQL mistakes, but it does not provide authorization, tenant isolation, safe serialization, or complete injection protection automatically.

  • Bind query values and allow-list any dynamic identifiers or query fragments.
  • Authorize the requested entity, not only the endpoint or supplied identifier.
  • Use DTOs so private fields and lazy graphs are not serialized accidentally.
  • Use a least-privilege PostgreSQL role without schema-owner or superuser rights.
  • Keep database credentials in a protected runtime secret store and rotate them.
  • Encrypt transport with verified TLS and configure certificate validation.
  • Do not log passwords, tokens, personal data, or raw bind parameters indiscriminately.
  • Apply tenant predicates and database protections consistently in multi-tenant systems.
  • Limit query size and complexity to reduce denial-of-service risk.

Mass assignment is a common merge-related security problem. Converting an entire request body to an entity and merging it can overwrite owner, price, role, status, version, or audit fields the caller was never allowed to change. Load the authorized managed entity and expose explicit domain operations for permitted changes.

21. Testing persistence behavior

Persistence tests must prove mappings, SQL behavior, constraints, lifecycle transitions, and concurrency against the database used in production.

  • Unit-test domain behavior without starting Spring when persistence is irrelevant.
  • Use focused JPA slice tests for mappings and queries.
  • Use Testcontainers with PostgreSQL for database-specific behavior.
  • Apply the same migrations used in production.
  • Flush inside tests so deferred constraint and version failures become visible.
  • Clear before rereading when the assertion must prove stored state rather than cached state.
  • Count statements for important fetch plans to prevent N+1 regressions.
  • Use separate transactions or threads to test real optimistic and pessimistic conflicts.
A test that proves database state
@Test
void confirmsOrder() {
    PurchaseOrder order = new PurchaseOrder("PO-2042", Money.usd("30.00"));
    entityManager.persist(order);
    entityManager.flush();
    entityManager.clear();

    PurchaseOrder reloaded = entityManager.find(PurchaseOrder.class, order.getId());
    reloaded.confirm();
    entityManager.flush();
    entityManager.clear();

    PurchaseOrder stored = entityManager.find(PurchaseOrder.class, order.getId());
    assertThat(stored.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
}
Rollback-only tests can hide commit-time failures

A test that never commits may miss deferred constraints, commit callbacks, or conflicts detected during transaction completion. Keep fast rollback tests, but add committed integration tests for behavior whose timing matters.

22. Edge cases and hard lessons

Detached graphs

A detached entity can contain initialized and uninitialized associations. Treating it as a complete snapshot is unsafe. Define a request contract, reload authoritative state, check the version, and apply explicit changes.

Proxy type checks

A lazy to-one value may be a generated subclass proxy. Strict getClass() equality, final classes, final accessors, and type-based serialization can behave unexpectedly. Use proxy-aware equality or provider utilities where necessary, and avoid exposing proxies outside the persistence layer.

Replacing managed collections

Replacing a Hibernate-managed collection wrapper with an arbitrary new list can confuse orphan tracking and generate excessive changes. Prefer domain methods that mutate the managed collection while maintaining both relationship sides.

Database defaults

An explicitly inserted null can prevent a database default from applying. Generated columns, timestamps, triggers, and defaults also require a clear refresh strategy if Java code needs their values immediately. Make one layer authoritative and test insert behavior.

Soft deletion

A deleted flag affects every query, unique constraint, association, cache entry, and foreign key. It also retains personal data. Use it only when the domain needs recovery or history. Prefer a dedicated audit trail for immutable history and define partial uniqueness in PostgreSQL when only active rows must be unique.

Time zones

Persist an Instant for a moment on the global timeline. Use LocalDate for a calendar date and LocalDateTime only when the value truly has no zone. Align JVM, JDBC, Hibernate, and PostgreSQL expectations and test daylight-saving boundaries.

23. Common mistakes and corrections

Mistake Consequence Better approach
Treating ORM as a reason not to learn SQL Slow and incorrect generated queries Inspect SQL, plans, indexes, and locks
Making every association eager Over-fetching and N+1 secondary selects Use lazy mappings and use-case fetch plans
Returning entities from REST endpoints Data leaks, recursion, and lazy queries Map to explicit response DTOs
Calling merge and continuing with the input Later changes are not tracked Use the managed value returned by merge
Cascading ALL everywhere Unexpected graph persistence and deletion Cascade only across true lifecycle ownership
Updating only the inverse relationship side Foreign key does not change as expected Update the owning side and keep both sides consistent
Using mutable fields in hashCode Entities disappear from hash collections Use a stable equality strategy
Depending on ddl-auto=update Unreviewed and unsafe schema evolution Use versioned migrations and validation
Ignoring persistence-context growth High memory and dirty-checking cost Flush and clear batches or use a lower-level tool
No version column Silent lost updates Use optimistic locking on concurrently edited entities
Long transaction around remote calls Pool exhaustion, lock waits, and deadlocks Use short transactions and reliable side-effect patterns
Testing only with an in-memory database PostgreSQL behavior is untested Use real PostgreSQL integration tests

24. Production readiness checklist

  • Let the supported Spring Boot platform manage Hibernate and Jakarta dependency versions.
  • Use reviewed, repeatable migrations and validate mappings at startup.
  • Disable Open EntityManager in View and map transaction-loaded results to DTOs.
  • Use explicit transaction boundaries around complete business operations.
  • Add version columns where lost updates matter.
  • Enforce nullability, uniqueness, checks, and referential integrity in PostgreSQL.
  • Index based on representative queries and inspect query plans.
  • Choose and test fetch plans for critical endpoints.
  • Configure batching only after proving it works with the identifier strategy.
  • Set connection, query, lock, and transaction timeouts.
  • Monitor SQL latency, statement counts, pool use, lock waits, and rollback rates.
  • Handle optimistic conflicts, deadlocks, and serialization failures deliberately.
  • Use least-privilege database credentials, TLS, secret rotation, and safe logging.
  • Test migrations and persistence behavior against the supported PostgreSQL version.
  • Review Hibernate and Spring migration guides before upgrades.

25. Interview questions and answers

What problem does ORM solve?

ORM maps object state and relationships to relational structures, manages entity identity and lifecycle, translates object changes into SQL, and reduces repetitive JDBC mapping. It does not replace schema design, SQL knowledge, transactions, or performance analysis.

What is the difference between JPA and Hibernate?

Jakarta Persistence is a standard API and specification. Hibernate ORM is a provider that implements it and offers additional native capabilities. Spring integrates either the standard or native API with transactions, resource management, and exception translation.

What is a persistence context?

It is a set of managed entity instances associated with an entity manager. It provides one Java instance per entity identity, tracks changes, coordinates lifecycle operations, and acts as the first-level cache and unit of work.

What are the entity lifecycle states?

New means not managed and without persistent identity. Managed means associated with the current persistence context. Detached means it has identity but is no longer managed. Removed means it is managed and scheduled for deletion.

How does dirty checking work?

Hibernate tracks the loaded state of managed entities. During flush it determines which values changed and schedules the corresponding SQL. No explicit update call is required for a managed entity.

What is the difference between flush and commit?

Flush executes SQL to synchronize in-memory managed state with the database inside the current transaction. Commit completes the transaction. Flushed changes can still roll back.

What is the first-level cache?

It is the mandatory cache inside one persistence context. Repeated identity lookups can return the same managed object without another row load. It is not shared between entity managers and should not be used as an application-wide cache.

What is the owning side of a relationship?

It is the side whose mapping writes the foreign key or join table. In a bidirectional one-to-many/many-to-one relationship, the many-to-one side owns the foreign key and the one-to-many side uses mappedBy.

How is cascade remove different from orphan removal?

Cascade remove deletes a target when the source entity itself is removed. Orphan removal deletes a privately owned child when it is disconnected from a one-to-one or one-to-many relationship.

What is the N+1 query problem?

One query loads N parent rows and then N additional queries initialize an association for each parent. Solve it with a use-case-specific fetch join, entity graph, projection, or appropriate batch fetch, and verify by counting SQL.

What is the difference between persist and merge?

Persist normally makes the same new instance managed and returns nothing. Merge copies state from a new or detached object into a managed instance and returns that instance. The merge input does not become managed.

How does Hibernate save differ?

The older Hibernate-native save operation historically persisted a transient object and returned its identifier. It was deprecated in Hibernate 6 in favor of JPA-style persist and is not part of the current Session contract. Spring Data's repository save is a different abstraction.

How does optimistic locking prevent lost updates?

A version value is read with the entity and included in update or delete conditions. If another transaction changed the row, the old version no longer matches, zero rows are affected, and the provider throws an optimistic locking exception.

When should pessimistic locking be used?

Use it when contention is expected and allowing work to proceed until a late optimistic failure is too expensive, or when a short critical section must reserve rows. Keep the transaction short and handle timeouts and deadlocks.

Compare the inheritance strategies.

Single table gives fast polymorphic reads but nullable subtype columns. Joined normalizes subtype data but requires joins. Table per class duplicates base columns and uses unions for polymorphism. Mapped superclass only shares mappings and has no polymorphic entity hierarchy.

Why can merge be dangerous for API updates?

A detached request-shaped object may omit fields or include fields the caller may not update. Merge treats its state as input to copy, which can overwrite authoritative values. Loading the managed entity and applying allowed commands is safer.

Why should EntityManager not be shared across threads?

It contains mutable identity maps, snapshots, proxies, queued actions, and transaction state and is explicitly not thread-safe. Each transaction or thread needs its own context.

Why is a DTO projection often faster for reads?

It can select only required columns, avoid building a managed entity graph, avoid dirty-checking snapshots, and prevent accidental lazy traversal. Use entities when behavior and updates need a managed model.

Why can collection fetch joins break pagination?

A join produces multiple SQL rows per parent. Limit and offset apply to rows before Hibernate reconstructs distinct parents. Page identifiers first and then fetch the graph, or use an appropriate projection.

Why use database constraints when entities validate data?

The database is the final authority across concurrent transactions and every writer. Application validation improves error messages but can race and can be bypassed.

26. Practice exercises

  1. Map an order aggregate with an embedded money value and owned order lines.
  2. Write a test proving new, managed, detached, and removed state transitions.
  3. Demonstrate first-level cache identity and then repeat after clear().
  4. Cause a deferred unique constraint violation and observe when flush reveals it.
  5. Write helper methods that keep both sides of a bidirectional relationship consistent.
  6. Create an N+1 problem, count the statements, and fix it with a purpose-built fetch plan.
  7. Compare JPQL, Criteria, and native PostgreSQL implementations of one search screen.
  8. Run two transactions that produce an optimistic-lock conflict and design the user response.
  9. Run two transactions with opposite lock order, observe deadlock handling, then fix the order.
  10. Benchmark sequence and identity identifier strategies with insert batching.
  11. Page a one-to-many result safely without a collection fetch-join pagination bug.
  12. Test the same migration and mapping against a PostgreSQL Testcontainer.

27. One-screen cheat sheet

JPA and Hibernate rules
  • JPA is the standard; Hibernate is an implementation and extension.
  • The persistence context is an identity map, first-level cache, and unit of work.
  • Managed entity changes are synchronized by dirty checking at flush.
  • Flush executes SQL inside the transaction; commit completes the transaction.
  • Persist manages the same new object; merge returns a managed copy.
  • The input to merge remains detached.
  • The owning relationship side writes the foreign key.
  • Cascade only across genuine lifecycle ownership.
  • Orphan removal deletes a privately owned disconnected child.
  • Prefer explicit fetch plans and DTOs over global eager loading.
  • Use @Version to detect stale writes.
  • Use pessimistic locks only in short, measured critical sections.
  • Bind query values and allow-list dynamic identifiers.
  • Use migrations and database constraints as the schema authority.
  • Inspect SQL, statement counts, plans, locks, and connection use.
  • Never share an EntityManager across threads.

28. Official references

Last reviewed · July 2026 · part of knowledge-base