Locking

This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — 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.

Three older reference books were consulted as bibliography only while preparing these pages and are not the primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the javax.persistencejakarta.persistence namespace change, the ORM 6 query-engine rewrite, the Hibernate Search 6+ Elasticsearch backend), so the official documentation above wins on any discrepancy.

This section’s bibliography lists the reference material consulted while preparing these pages.

Concurrent writers to the same row need a strategy for who wins. This page covers Hibernate’s optimistic and pessimistic locking mechanisms in depth; for how Spring layers @Transactional isolation and @Lock on top of these, see Transaction Isolation & Locking.

Optimistic locking with @Version

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

    @Version
    private Long version;

    private BigDecimal balance;
}

Every UPDATE Hibernate generates for a @Version-annotated entity includes WHERE id = ? AND version = ? and increments the version column. If zero rows match (because another transaction already updated — and incremented the version of — that row first), Hibernate throws OptimisticLockException (JPA) / StaleObjectStateException (native) instead of silently overwriting the concurrent change. This needs no database-level locking at all — the conflict is only ever detected at write time, which is why it is called optimistic: it assumes conflicts are rare and pays the cost only when one actually occurs.

OptimisticLockType/LockModeType variants:

Mode Behavior

OPTIMISTIC (JPA LockModeType.OPTIMISTIC)

Standard @Version check as above, applied at flush.

OPTIMISTIC_FORCE_INCREMENT

Forces a version increment even when the entity’s own state did not change — used when a change to an associated entity should still be treated as a conflict-relevant change to this entity (e.g. incrementing an Order’s version when an `OrderLine is added, so a concurrent edit to the order sees the line addition as a conflict too).

Versionless optimistic locking (@OptimisticLocking(type = OptimisticLockType.ALL))

No @Version column at all — the WHERE clause instead compares every mapped column against the values loaded, detecting a conflict on any changed column. Works without a schema change but is more fragile (concurrent updates to unrelated columns still "win" against each other correctly, but the check is heavier and less explicit) — @Version is the generally preferred approach when a schema change is possible.

@OptimisticLocking: dirty vs. all

@OptimisticLocking(type = OptimisticLockType.DIRTY) narrows the versionless comparison (or, combined with @DynamicUpdate, the @Version-based one) to only the columns that actually changed in this flush, rather than every mapped column — reduces the chance of a false conflict against a concurrent change to an unrelated column, at the cost of needing @DynamicUpdate (see Flushing & Dirty Checking) since the generated SQL now varies per flush.

Pessimistic locking

Pessimistic locking asks the database to hold a row lock for the duration of the current transaction, blocking (or failing) other transactions that want a conflicting lock on the same row — suited to high-contention scenarios where an optimistic conflict would be common and retrying is expensive (e.g. decrementing a limited inventory count).

LockModeType Behavior

PESSIMISTIC_READ

A shared lock — blocks other transactions from acquiring a conflicting write lock, but not from also reading. Maps to SELECT …​ FOR SHARE (or the dialect’s equivalent).

PESSIMISTIC_WRITE

An exclusive lock — blocks any other transaction from reading-for-update or writing the row until this transaction ends. Maps to SELECT …​ FOR UPDATE.

PESSIMISTIC_FORCE_INCREMENT

Combines a pessimistic write lock with an immediate @Version increment, for when a concurrently-running optimistic reader elsewhere should also see this as a conflict once it later tries to commit.

Account account = em.find(Account.class, id, LockModeType.PESSIMISTIC_WRITE);
// or, explicitly against an already-loaded managed entity:
em.lock(account, LockModeType.PESSIMISTIC_WRITE);

Map<String, Object> hints = Map.of("jakarta.persistence.lock.timeout", 3000); // milliseconds
Account locked = em.find(Account.class, id, LockModeType.PESSIMISTIC_WRITE, hints);

Hibernate’s native Session.lock(entity, LockMode) and Session.get(Class, id, LockOptions) offer the same capability with finer-grained LockOptions (scope, timeout, and lockScope = LockOptions.Scope.EXTENDED to also lock owned collections).

Lock scope and timeout hints

jakarta.persistence.lock.timeout (milliseconds; 0 = no wait/fail-fast, negative = wait indefinitely per the database’s default) controls how long a pessimistic-lock acquisition waits before the database raises a lock — wait — timeout error, translated by Hibernate into jakarta.persistence.PessimisticLockException / LockTimeoutException. jakarta.persistence.lock.scope = PessimisticLockScope.EXTENDED extends a pessimistic lock on an entity to also lock rows in its owned collection tables, not just the entity’s own row.

Optimistic vs. pessimistic: choosing

Transaction Isolation & Locking already lays out the general decision (contention frequency, retry cost, UX for conflicts). At the Hibernate-mapping level specifically: optimistic locking needs no schema change beyond a @Version column and scales better under low contention (no lock held across a possibly-slow transaction); pessimistic locking avoids retries entirely under high contention but holds a real database lock for as long as the transaction runs, directly limiting throughput on that row.