DevNotes
Support
Spring Data JPA & Hibernate · Chapter 08

Persistence Context & Entity Lifecycle

The persistence context as a first-level cache and unit of work, the four entity states, persist, merge, remove and detach, dirty checking, flush modes and timing, getReferenceById, LazyInitializationException, Open Session in View and lifecycle callbacks.

◷ 7 min read§ 12 sections? 8 interview questions{ } 7 code examples

#Introduction

Most "JPA magic" — updates without save(), lazy loading, LazyInitializationException, stale data after bulk updates — is explained by one concept: the persistence context.

#What Is the Persistence Context?

The persistence context is the set of entity instances an EntityManager (Hibernate Session) is currently managing. In Spring it normally lives for the duration of a transaction.

It acts as:

  • a first-level cache — loading the same id twice returns the same object without a second query;
  • an identity map — at most one managed instance per entity id;
  • a unit of work — it tracks changes and writes them at flush time.
Java
@Transactional
public void demo() {
    Employee a = repository.findById(1L).orElseThrow();   // SELECT
    Employee b = repository.findById(1L).orElseThrow();   // no SQL — same persistence context
    System.out.println(a == b);                            // true
}

#The Four Entity States

new Employee() — TRANSIENT: not known to JPA, no id
persist() / save() — MANAGED: tracked by the persistence context
Transaction ends, or detach() / clear() — DETACHED: has an id, no longer tracked
merge() — changes copied onto a MANAGED instance again
remove() — REMOVED: deleted at flush
StateIn persistence context?In database?Changes saved automatically?
Transient (new)❌❌❌
Managed (persistent)✅✅ (or will be at flush)✅
Detached❌✅❌
Removed✅ (scheduled for deletion)Until flush—

#persist, merge, remove and detach

Java
Employee e = new Employee("Asha");   // transient
em.persist(e);                       // managed — INSERT at flush (or immediately with IDENTITY)

em.detach(e);                        // detached — further changes ignored
e.setName("Asha R");

Employee managed = em.merge(e);      // copies state onto a managed instance
// managed != e — 'e' itself stays detached!

em.remove(managed);                  // removed — DELETE at flush
OperationBehaviour
persist(transient)Makes it managed; no return value
persist(detached)Throws (EntityExistsException / PersistentObjectException)
merge(detached or transient)Loads (SELECT) or creates the managed copy, copies state, returns it
remove(managed)Schedules a delete; remove(detached) throws IllegalArgumentException
detach(e) / clear()Stops tracking one / all entities
refresh(e)Reloads state from the database, overwriting changes

#Dirty Checking

When an entity becomes managed, Hibernate keeps a snapshot of its loaded state. At flush time it compares each managed entity with its snapshot and issues UPDATEs for the ones that changed.

Java
@Transactional
public void rename(Long id, String newName) {
    Employee e = repository.findById(id).orElseThrow();   // managed + snapshot
    e.setName(newName);                                    // just a field change
}   // commit → flush → dirty check → UPDATE employees SET name=? … WHERE id=?

No save() call is needed. If nothing changed, no UPDATE runs.

By default Hibernate updates all columns; @DynamicUpdate on the entity makes it include only changed columns.

#Flushing

Flush = sending pending SQL (INSERT/UPDATE/DELETE) to the database. It's not a commit — the transaction can still roll back.

Hibernate flushes automatically (FlushModeType.AUTO):

  • before a transaction commits;
  • before a query whose results could be affected by pending changes (so queries see your own writes);
  • when you call flush() / saveAndFlush() explicitly.
Java
@Transactional
public void example() {
    Employee e = new Employee("Ravi");
    repository.save(e);                     // with SEQUENCE: no SQL yet

    long count = repository.countByName("Ravi");   // auto-flush → INSERT, then the count query
}

Hibernate also orders statements at flush: inserts, then updates, then collection changes, then deletes.

#getReferenceById — Proxies Without a Query

Java
Department dept = departmentRepository.getReferenceById(10L);   // no SELECT — a lazy proxy
employee.setDepartment(dept);                                    // only the id is needed for the FK

Perfect for setting a foreign key without loading the parent. Accessing a non-id field on the proxy triggers the SELECT; if the row doesn't exist, you get EntityNotFoundException at that point.

#LazyInitializationException

Java
public EmployeeDto get(Long id) {                   // no @Transactional
    Employee e = repository.findById(id).orElseThrow();
    return new EmployeeDto(e.getName(), e.getDepartment().getName());   // 💥
}
Output
org.hibernate.LazyInitializationException: could not initialize proxy [Department#7] - no Session

findById ran in its own short transaction; by the time you touch the lazy department proxy, the persistence context is closed, so Hibernate can't load it.

Fixes (in order of preference):

  1. Fetch what you need in the query — JOIN FETCH, @EntityGraph, or a DTO projection.
  2. Do the work inside a @Transactional service method and map to DTOs before returning.
  3. Not: FetchType.EAGER everywhere, or hibernate.enable_lazy_load_no_trans=true (opens a new session per lazy load — an N+1 machine).

#Open Session in View (OSIV)

Spring Boot enables spring.jpa.open-in-view=true by default and logs a warning about it. OSIV keeps the persistence context open for the whole HTTP request, including view rendering and JSON serialization, so lazy loading "just works" in controllers.

OSIV onOSIV off (spring.jpa.open-in-view=false)
No LazyInitializationException in controllersLazy access outside a transaction fails fast
Hidden lazy queries during serialization (N+1)Data needs are explicit in queries
Holds a database connection for the whole requestConnection released after the transaction

#Lifecycle Callbacks

Java
@Entity
public class Employee {
    private Instant createdAt;
    private Instant updatedAt;

    @PrePersist
    void onCreate() { createdAt = updatedAt = Instant.now(); }

    @PreUpdate
    void onUpdate() { updatedAt = Instant.now(); }
}

Available: @PrePersist, @PostPersist, @PreUpdate, @PostUpdate, @PreRemove, @PostRemove, @PostLoad — on the entity or in an @EntityListeners class. They don't run for bulk JPQL/native statements. For created/modified fields, Spring Data auditing (see Projections, Specifications & Auditing) is usually cleaner.

#Interview Questions

Q1What is the persistence context?

The set of managed entity instances for an EntityManager — a first-level cache and unit of work that guarantees one instance per id and tracks changes until flush. In Spring it's usually transaction-scoped.

Q2What are the JPA entity states?

Transient (new, not tracked), managed (tracked; changes saved automatically), detached (has an id but isn't tracked) and removed (scheduled for deletion).

Q3What is dirty checking?

Hibernate compares managed entities with snapshots of their loaded state at flush time and generates UPDATE statements for changed ones — so updates inside a transaction need no save().

Q4persist vs merge?

persist makes a new instance managed (and fails for detached ones). merge copies the state of a detached or new object onto a managed instance — loading it if needed — and returns that managed instance.

Q5flush vs commit?

Flush sends pending SQL to the database within the transaction; commit makes the transaction's changes permanent. A flushed change can still be rolled back.

Q6What causes LazyInitializationException and how do you fix it?

Accessing an uninitialised lazy association after the persistence context has closed. Fix it by fetching the data you need (JOIN FETCH, entity graphs, DTOs) inside the transaction — not by making everything EAGER.

Q7What is Open Session in View and should you use it?

It keeps the persistence context open for the whole web request so lazy loading works in the view layer. It hides N+1 queries and holds connections longer; many teams disable it for APIs.

Q8findById vs getReferenceById?

findById queries immediately (or uses the first-level cache) and returns Optional. getReferenceById returns a lazy proxy without a query — ideal for setting foreign keys — and fails only when you access missing data.

#Key Takeaways

  • The persistence context = first-level cache + identity map + unit of work.
  • Managed entities are saved automatically via dirty checking; detached ones aren't.
  • merge returns the managed instance — use it.
  • Flush ≠ commit; Hibernate auto-flushes before commits and relevant queries.
  • Fix LazyInitializationException with explicit fetching, not EAGER or OSIV.