Book.withTransaction {
def book = Book.get(1)
book.lock() // unchanged: lock without refreshing
book.title = 'Updated title'
book.save(failOnError: true)
}
lock
Purpose
The lock method obtains a pessimistic lock on a database row, typically using SQL select … for update. The instance method entity.lock() locks an already-loaded instance. The static method DomainClass.lock(id) loads and locks by identifier, and DomainClass.lock(id, refresh: true) additionally reloads the state and version of an instance that is already managed in the current session. To reload an already-loaded instance under a pessimistic write lock, use refresh(lock: true).
Examples
Locking an already-loaded instance:
Loading and locking by identifier:
Book.withTransaction {
def book = Book.lock(1)
book.title = 'Updated title'
book.save(failOnError: true)
}
Locking by identifier and reloading an already-managed instance under the lock:
Book.withTransaction {
def book = Book.get(1)
// ... work that may leave book stale relative to the database ...
book = Book.lock(1, refresh: true) // same managed instance, reloaded under the lock
if (book.title == 'Draft') {
book.title = 'Ready for review'
book.save(failOnError: true)
}
}
Description
Pessimistic locking requires an active transaction. Use withTransaction as above or a GORM @Transactional method, and keep the lock and the work it protects in the same transaction. The lock is held until that transaction commits or rolls back, not simply until a controller action returns.
The instance method entity.lock() is unchanged: it acquires a lock without reloading the entity’s state. For an already-loaded, versioned entity, Hibernate still checks its version against the database; a concurrent update between loading and locking can therefore cause an optimistic locking failure.
The static DomainClass.lock(id) method is also unchanged, and DomainClass.lock(id, refresh: false) behaves the same: both lock without refreshing. When the entity is not yet in the session it is loaded under the lock, avoiding a separate unlocked get(). When the entity is already managed in the current session, its already-loaded state is locked and version-checked rather than reloaded.
The forms that take named arguments - lock(id, refresh: true) and any call passing type - require an active
transaction and throw jakarta.persistence.TransactionRequiredException without one, before anything is loaded,
including for a null identifier. A type supplied as null counts as absent: the call takes the default lock
and behaves exactly like lock(id), transaction requirement included.
lock(id), lock(id, refresh: false) and the instance method entity.lock() keep the behaviour they have
always had, which differs by implementation. GORM for Hibernate 7 rejects them without a transaction, with the
same exception. GORM for Hibernate 5 does not: the call returns the instance and getCurrentLockMode reports
PESSIMISTIC_WRITE, but the statement ran on an auto-commit connection, so the row lock is gone as soon as it
completes and another transaction can take it immediately. Always lock inside a transaction.
Pass type to choose the lock mode. It accepts a jakarta.persistence.LockModeType or its name and defaults to PESSIMISTIC_WRITE; NONE is rejected. For example Book.lock(1, type: LockModeType.PESSIMISTIC_READ) acquires a pessimistic READ lock, whether the entity is loaded as part of the call or is already managed in the session. The semantics of each mode are those of the JPA LockModeType of the same name. Datastores that support only a pessimistic write lock throw UnsupportedOperationException for any other type.
The lock modes behave as follows on Hibernate 5 and Hibernate 7:
| Lock mode | Database lock | Effect |
|---|---|---|
|
Exclusive row lock, typically |
The default, so |
|
Shared row lock, |
Other transactions cannot update the row but may take the same shared lock. Databases without shared row locks take an exclusive lock instead. |
|
Exclusive row lock |
As |
|
None |
The version is re-read when the transaction commits, and the commit fails if another transaction changed it. |
|
None |
The version is incremented when the transaction commits, so other transactions holding the previous version fail. |
|
None |
Rejected with |
Pass refresh: true to reload instead. DomainClass.lock(id, refresh: true) behaves like DomainClass.lock(id), except that when the entity is already managed in the current session it reloads that instance’s state and version under the lock and returns that same managed instance. When the entity is not in the session it is loaded under the lock. Both forms return null when no row exists for the identifier. Calling lock(id, refresh: true) without an active transaction throws jakarta.persistence.TransactionRequiredException before anything is loaded. On a named connection, DomainClass.secondary.lock(id, refresh: true) performs the whole operation on that connection and requires a transaction on it.
refresh: true discards unflushed changes to the entity. Configured refresh cascades can also discard unflushed changes to associated entities; this is not a refresh of the whole object graph. Call it before making decisions based on the entity’s state or applying mutations.
|
Refreshing under a lock is supported by GORM for Hibernate 5 and Hibernate 7. Other datastores throw UnsupportedOperationException rather than silently falling back to a lock without a refresh.
There is no entity.lock(refresh: true). Groovy resolves that call to the static lock(Serializable id) method with the options map as the identifier, and GORM rejects it with an IllegalArgumentException that names the supported forms. To reload an already-loaded instance under a pessimistic write lock, use refresh(lock: true); to choose another lock mode, pass a jakarta.persistence.LockModeType as the lock argument of refresh.
|
Pessimistic locking does not guarantee freedom from deadlocks, lock timeouts, or transaction serialization failures. Locking and read visibility depend on the database and transaction isolation level; ordinary reads are not necessarily blocked by a write lock.
Refer to the section on Optimistic and Pessimistic locking in the GORM for Hibernate 7 or Hibernate 5 guide for info.