💾 Section 16 · Question #15

What Does the refresh() Method Do

The refresh() method in JPA (entityManager.refresh(entity)) forcefully reloads the state of a Managed entity directly from the relational database via an immediate SQL SELECT by...

The refresh() method in JPA (entityManager.refresh(entity)) forcefully reloads the state of a Managed entity directly from the relational database via an immediate SQL SELECT by primary key. It resynchronizes the entity’s in-memory Java fields with current database column values, irrevocably overwriting any unsaved local modifications and resetting the First-Level Cache dirty checking snapshot.


🟢 Junior Level

Core Purpose and Fundamental Behavior

By default, Hibernate relies on the First-Level Cache (L1): once an entity is managed in a session, subsequent find() invocations return the in-memory instance without querying the database. The refresh() method bypasses the L1 cache, forces a physical SQL SELECT against the database, and replaces the object’s current field values with fresh database state:

@Transactional
public void demonstrateRefresh() {
    User user = entityManager.find(User.class, 1L); // user.getName() == "Ivan"
    
    // 1. Mutate state locally in JVM memory
    user.setName("Temporary Name"); 

    // 2. Force database re-read
    entityManager.refresh(user); 
    // Hibernate executes: SELECT id, name, email FROM users WHERE id = 1;
    
    // 3. Local in-memory changes are completely overwritten!
    System.out.println(user.getName()); // Prints "Ivan" from database!
    
    // At transaction commit, ZERO SQL UPDATE statements are generated.
}

Key Rules of refresh()

  1. Requires Managed Lifecycle State: Passing an entity in the Detached or Transient state immediately triggers: java.lang.IllegalArgumentException: Entity not managed.
  2. Discards Unsaved In-Memory Mutations: All property changes made in Java before invoking refresh() are permanently lost (unless explicitly synchronized via flush() prior to refresh).
  3. Resets Dirty Checking Snapshots: The baseline snapshot (loadedState) in the L1 cache is updated with the fresh database values, marking the entity as clean.
                      REFRESH() EXECUTION LIFECYCLE
                      
  [ JVM Heap: User@0x1 ]                  [ Relational Database: users ]
    name = "Changed in Memory"              id = 1, name = "Ivan in DB"
              │                                      │
              │──── 1. entityManager.refresh(user) ──│
              │                                      │
              │<─── 2. SQL SELECT * WHERE id = 1 ────│
              │     (Fetches committed DB row)       │
              ▼                                      ▼
  [ JVM Heap: User@0x1 ]                  [ Relational Database: users ]
    name = "Ivan in DB"                     id = 1, name = "Ivan in DB"
    (Snapshot updated; UPDATE canceled)

🟡 Middle Level

Production Use Cases for refresh()

  1. Database Triggers and Stored Procedures: When relational database triggers (BEFORE INSERT / BEFORE UPDATE) calculate column values (e.g., promotional codes, discount totals, status flags), Hibernate remains unaware of these backend computations.
    entityManager.persist(order);
    entityManager.flush(); // Sends physical INSERT; database trigger fires
    entityManager.refresh(order); // Reloads trigger-computed columns into memory
    
  2. Retrieving Database Default Values (DEFAULT): When table columns are initialized by the database engine (created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP), calling refresh() fetches the server-generated timestamp.
  3. Synchronizing After Direct Bulk Operations (@Modifying): Bulk updates (UPDATE User u SET u.active = false) execute directly against the database engine, bypassing the active PersistenceContext. Entities already held in memory become stale. Calling refresh() resynchronizes them with the database.

Pessimistic Locking with refresh()

The refresh() method allows developers to acquire pessimistic database locks dynamically on already loaded entities within a single SQL statement:

// Upgrades to an exclusive row-level lock: SELECT ... FOR UPDATE
entityManager.refresh(account, LockModeType.PESSIMISTIC_WRITE);

// Pessimistic lock with lock acquisition timeout
Map<String, Object> hints = Map.of("jakarta.persistence.lock.timeout", 3000); // 3 seconds timeout
entityManager.refresh(account, LockModeType.PESSIMISTIC_WRITE, hints);

Cascading Refresh (CascadeType.REFRESH)

By default, calling refresh(order) refreshes only the scalar columns of Order. Related child collections are not refreshed unless cascading is explicitly declared:

@Entity
public class Order {
    @Id
    private Long id;

    @OneToMany(mappedBy = "order", cascade = CascadeType.REFRESH)
    private List<OrderItem> items = new ArrayList<>();
}

When entityManager.refresh(order) is called, Hibernate automatically executes a cascading refresh() on each associated OrderItem element.


🔴 Senior Level

Internal Engine Mechanics: StatefulPersistenceContext Impact

Inside Hibernate (SessionImpl), refresh() is processed by DefaultRefreshEventListener:

  1. It validates the entity’s status in entityEntries. If the entity is not Status.MANAGED, it aborts with an IllegalArgumentException.
  2. Any pending actions targeting the entity in the ActionQueue are evaluated.
  3. Hibernate constructs an SQL SELECT covering all mapped columns and executes it over JDBC.
  4. Extracted column values are written into the target Java object instance (via direct field reflection or property setters).
  5. The Critical Snapshot Step: The Object[] loadedState array inside EntityEntry is completely overwritten with the fresh column values. Consequently, subsequent dirty checking scans evaluate the entity as unchanged (not dirty).
  6. Collection Handling (PersistentCollection):
    • If an associated collection is uninitialized, it remains an uninitialized lazy proxy.
    • If an associated collection is already loaded in memory, Hibernate clears its cached elements and re-initializes it from the database.

Modern Hibernate 6 Alternative: @Generated

In modern applications running on Hibernate 6+, manually calling flush() + refresh() to retrieve database-computed defaults or trigger values is considered an anti-pattern. Hibernate provides the @Generated annotation:

@Entity
public class Invoice {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // Hibernate 6 automatically populates this via SQL RETURNING / post-insert read
    @Generated(event = EventType.INSERT)
    @Column(name = "created_at", insertable = false, updatable = false)
    private Instant createdAt;

    @Generated(event = {EventType.INSERT, EventType.UPDATE})
    @Column(name = "total_amount", insertable = false, updatable = false)
    private BigDecimal totalAmount;
}

When using @Generated, Hibernate utilizes PostgreSQL’s INSERT ... RETURNING created_at, total_amount (or executes an immediate post-insert query), completely eliminating the need for manual flush() and refresh() calls.


4 Tricky Questions

1. What happens if you modify an entity field user.setEmail("new@mail.com"), call entityManager.refresh(user), and subsequently commit the transaction? Will an SQL UPDATE be issued?

Answer:

  1. Zero SQL UPDATE statements will be executed.
  2. The refresh() method executes an immediate SQL SELECT, reads the original email from the database, and overwrites the field in JVM heap memory.
  3. Simultaneously, Hibernate updates the loadedState snapshot inside the PersistenceContext to reflect the fresh database value.
  4. During transaction commit, Dirty Checking compares the current in-memory field with the updated snapshot. Finding zero differences, Hibernate cancels update scheduling.

2. What happens if you call entityManager.refresh(user), but the underlying database row was deleted by a concurrent database transaction?

Answer:

  1. The refresh() method executes SELECT * FROM users WHERE id = ?.
  2. The database returns an empty ResultSet (0 rows found).
  3. The JPA specification mandates that Hibernate immediately throw: jakarta.persistence.EntityNotFoundException: Entity no longer exists in the database.
  4. The active transaction is automatically marked as Rollback-Only.

3. We executed a Spring Data JPA query: @Modifying @Query("UPDATE User u SET u.active = false"). Why do already loaded entities still reflect active = true without calling refresh()?

Answer:

  1. The @Modifying annotation executes a direct bulk SQL/JPQL UPDATE against the database table, completely bypassing the First-Level Cache (PersistenceContext).
  2. Entities already loaded into the current session have no awareness of the out-of-band database update and continue holding their initial in-memory state.
  3. To resynchronize, you must either invoke entityManager.refresh(user) on each entity or configure @Modifying(clearAutomatically = true). The clearAutomatically = true setting clears the entire L1 cache, forcing subsequent find() queries to re-read fresh records from the database.

4. Can invoking refresh() cause a NonUniqueObjectException if the session contains associated entities?

Answer:

  1. For the target entity being refreshed, identity collisions are impossible because Hibernate updates the existing Java instance in-place according to the Identity Map pattern.
  2. However, if a cascading refresh(order) reloads a collection containing child entities that were previously detached and re-instantiated elsewhere in code outside the session, attempting to associate the duplicate child instance can trigger an identity collision in the PersistenceContext.

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“The entityManager.refresh() method forcefully reloads a Managed entity’s state from the database via an immediate SQL SELECT by primary key. It discards unsaved in-memory property changes and updates the entity’s baseline snapshot (loadedState) in the First-Level Cache, preventing unwanted UPDATE statements on commit. Key production use cases include capturing database trigger calculations, loading database defaults, resynchronizing after bulk @Modifying queries, and dynamically acquiring pessimistic locks via refresh(entity, LockModeType.PESSIMISTIC_WRITE). In Hibernate 6+, using @Generated is preferred over manual refresh() for database-computed columns.”

Key Production Metrics & Health Checks

  • SELECT Query Rate per Transaction: Excessive refresh() calls indicate poor caching design or redundant reads of already managed data.
  • EntityNotFoundException Frequency: Spikes in this metric highlight concurrency issues where rows are deleted externally before transactional sync completes.

Red Flags to Avoid

  • ❌ “I call refresh() after every persist() to verify the row was saved properly.” — A severe anti-pattern that doubles database queries without providing any real guarantees.
  • ❌ “You can call refresh() on detached entities or REST DTOs.” — refresh() strictly requires entities in the Managed state; it throws IllegalArgumentException otherwise.
  • ❌ “Calling refresh() pushes in-memory changes to the database.” — Completely backwards: flush() pushes to the DB; refresh() reads from the DB and overwrites memory.
  • ❌ “Calling refresh() commits the transaction.” — refresh() only executes a read query; the transaction remains open.