In plain JPA/Hibernate, how do you tell the persistence provider to read an entity while holding a database row lock for the rest of the transaction, and what does it actually send to the database?
answer
- find(id, LockModeType) = read + lock in one SQL
- setLockMode on a query; em.lock upgrades a managed entity
- FOR UPDATE, held until commit/rollback
- no transaction → TransactionRequiredException
- pessimistic read bypasses the L2 cache
basics
~20 sPass a pessimistic lock mode on the read: em.find(Order.class, id, LockModeType.PESSIMISTIC_WRITE), query.setLockMode(...), or em.lock(managedEntity, ...). Hibernate emits SELECT ... FOR UPDATE. A transaction must be active, and the database holds the lock until commit or rollback.
solid answer
~40 sThree entry points, all taking a `LockModeType`: - `em.find(Order.class, id, LockModeType.PESSIMISTIC_WRITE)` — loads and locks in one statement. - `query.setLockMode(LockModeType.PESSIMISTIC_WRITE)` on a JPQL/Criteria query — locks every row the query returns. - `em.lock(order, LockModeType.PESSIMISTIC_WRITE)` — locks an entity already managed in this persistence context. Hibernate maps the mode through the dialect to `SELECT ... FOR UPDATE` (`FOR SHARE` for `PESSIMISTIC_READ`). A pessimistic read goes to the database and bypasses the second-level cache — a cached copy proves nothing about who holds the row now. An active transaction is mandatory; without one JPA throws `TransactionRequiredException`. The lock belongs to the database, not to Hibernate, so only commit or rollback releases it — clearing or detaching the entity does not. Native equivalent: `session.find(Order.class, id, LockMode.PESSIMISTIC_WRITE)`.
code
java · 16 linesem.getTransaction().begin();
// 1. read and lock in one statement
Order order = em.find(Order.class, id, LockModeType.PESSIMISTIC_WRITE);
// 2. lock every row a query returns
List<Order> batch = em.createQuery(
"select o from Order o where o.status = :s", Order.class)
.setParameter("s", Status.NEW)
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.getResultList();
// 3. upgrade an entity already managed here
em.lock(order, LockModeType.PESSIMISTIC_WRITE);
em.getTransaction().commit(); // locks released herego deeper
Name the three call sites and say the generated SQL is SELECT ... FOR UPDATE inside an active transaction, released at commit.
Add that the lock is the database's, held to the end of the transaction, that the L2 cache is bypassed, and that a missing transaction throws TransactionRequiredException.
Discuss lock duration as a throughput budget, the staleness gap when locking an already-loaded unversioned entity, and why FOR UPDATE plus outer joins is invalid SQL on most engines.
Frame it as choosing where serialization happens — a lock held across slow work converts a concurrency problem into a queueing problem, so bound the critical section and the connection hold time by design.
## The idea Pessimistic locking assumes a conflict is likely, so it takes a lock on the row *before* the read-modify-write and makes competing transactions wait. Serialization happens in the database, up front, rather than being detected when you try to write. ## The three ways to ask **Locking find.** `em.find(Order.class, id, LockModeType.PESSIMISTIC_WRITE)` issues one statement — `select ... from orders where id = ? for update`. This is the safest form: the row is read and locked atomically, so what you hold in memory is the state as of the moment the lock was granted. **Query-level lock.** `TypedQuery.setLockMode(...)` appends the `FOR UPDATE` clause to the generated query, locking every row in the result set. Watch out: most databases reject `FOR UPDATE` combined with an outer join, `DISTINCT` or `GROUP BY`, so a query that also `join fetch`es a collection will usually fail or lock the wrong table. **Locking an already-managed entity.** `em.lock(order, LockModeType.PESSIMISTIC_WRITE)` sends a separate lock statement for an entity loaded earlier in the same transaction. The subtlety: the copy in your persistence context was read *before* the lock existed, so it may already be stale. For an entity with a version attribute Hibernate puts the version in the lock statement's `where` clause and fails with an optimistic-lock error if the row moved on; for an entity with no version there is no such check, so follow the lock with `em.refresh(...)` if the values matter. ## What comes out on the wire The dialect decides the exact syntax: `for update` on PostgreSQL/Oracle/MySQL, `with (updlock, rowlock)` style hints on SQL Server. When several tables are involved Hibernate may render `for update of <alias>` so only the target table's rows are locked. ## Transaction and cache rules A pessimistic lock has no meaning outside a transaction, so JPA mandates `TransactionRequiredException` when none is active. The lock lives until the transaction ends — Hibernate cannot release it early, and there is no `unlock()`. That makes lock duration a design concern: everything you do between acquiring the lock and committing is time other requests spend blocked, and the JDBC connection is pinned for that whole window. Because correctness depends on the current row, Hibernate skips the second-level cache for pessimistically locked reads and goes to the database. ## Practical shape The usual pattern is: begin transaction, locking-find the row, validate and mutate, commit. Keep external calls (HTTP, mail, file IO) outside that window; a lock held across a slow remote call is how a healthy system turns into a queue of blocked threads.
- What happens if you call em.lock with a pessimistic mode outside a transaction?JPA specifies `TransactionRequiredException`. A row lock only exists inside a database transaction, so there is nothing meaningful the provider could do — it cannot acquire a lock that would be released immediately by an autocommit statement. The same rule applies to a locking `find` and to `Query.setLockMode`.
- Does em.lock refresh the entity you already loaded?Not in general. Hibernate sends a lock statement for the row, but the field values in your persistence context are still the ones read earlier. If the entity has a version attribute, Hibernate includes it in the lock statement and throws an optimistic-lock failure when the row changed; without a version you can silently be holding stale values, so either use a locking `find` from the start or call `em.refresh` after locking.
saying these in an interview costs you the question
- Thinking Hibernate itself holds the lock in the JVM, so clearing the persistence context or detaching the entity releases it.
- Believing a lock can be released before commit, or looking for an unlock() method.
- Expecting a pessimistic read to be served from the second-level cache.
- Adding join fetch to a query with setLockMode and assuming FOR UPDATE will still be valid SQL.