liveBook · Manning

Lost update anomaly

As explained in Java Persistence with Spring Data and Hibernate epub

A lost update occurs when one concurrent transaction overwrites another transaction's committed modification without detecting the conflict.

A lost update occurs when two transactions read the same value, change it independently, and write their results without detecting a conflict. The later write overwrites the earlier one: the last commit wins. This is a risk when concurrent transactions are not isolated by concurrency control. section 11.2.1

Two changes to one price

Two transactions read the same item’s buy-now price. Each calculates a new price and writes it. If the second write overwrites the first, the database retains only one change even though both transactions updated the field. section 11.2.1

Figure 11.1 Lost update: Two transactions update the same data without isolation.

The transaction timeline shows where two updates to the same data collide and one is lost. section 11.2.1

Optimistic versioning detects this conflict at write time. Map a version field with `@Version`; Hibernate reads its current value with the entity, then includes that value in the update condition and increments it. If another transaction has already changed the row’s version, the stale update fails with an `OptimisticLockException` rather than silently replacing the committed change. This is a version check, not an exclusive lock. section 11.2.2

Listing 11.2 Hibernate incrementing and checking the version automatically
Path: /Ch11/transactions/src/test/java/com/manning/javapersistence/ch11
 /concurrency/Versioning.java – firstCommitWins()
 
   EntityManager em1 = emf.createEntityManager();
   em1.getTransaction().begin();
Item item = em1.find(Item.class, ITEM_ID);                             #1
// select * from ITEM where ID = ?
assertEquals(0, item.getVersion());                                    #2
item.setName("New Name");
// . . .  Another transaction changes the record
assertThrows(OptimisticLockException.class, () -> em1.flush());        #3
// update ITEM set NAME = ?, VERSION = 1 where ID = ? and VERSION = 0
#1 Retrieving an entity instance by identifier loads the current version from the database with a <kbd>SELECT</kbd>.
#2 The current version of the <kbd>Item</kbd> instance is <kbd>0</kbd>.
#3 When the persistence context is flushed, Hibernate detects the dirty <kbd>Item</kbd> instance and increments its version to <kbd>1</kbd>. SQL <kbd>UPDATE</kbd> now performs the version check, storing the new version in the database, but only if the database version is still <kbd>0</kbd>.

The example follows a versioned `Item` from a read at version 0 to a failed update whose condition still requires version 0. section 11.2.2

A version also matters when an entity is detached and later merged: its old property values may no longer be available for comparison, so the detached instance must carry a version number or timestamp. Checking only changed properties can miss a meaningful conflict—for example, a concurrent description change while the price is changed. section 11.2.2

How Java Persistence with Spring Data and Hibernate epub covers it

How deep it goes
Traces the competing reads and writes, then shows version-based detection through a conditional update and exception.
Examples
Uses competing buy-now price updates and a versioned `Item` update that fails at flush.
What it assumes
Assumes basic database transaction and JPA entity familiarity; explains the conflicting transaction sequence explicitly.
What you can do after
Diagnose/debug a lost update by tracing concurrent writes; apply JPA `@Version` to detect stale updates.
How it connects
Connects transaction isolation, optimistic concurrency control, entity versioning, and detached-entity merging.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Java Persistence with Spring Data and Hibernate epub. The section links point to the parts of the book it draws on.