1. Overview
In this quick tutorial, we’ll discuss enabling transaction locks in Spring Data JPA for custom query methods and predefined repository CRUD methods.
We’ll also learn about different lock types and setting transaction lock timeouts.
2. Lock Types
JPA has two main lock types defined, Pessimistic Locking and Optimistic Locking.
2.1. Pessimistic Locking
When we use Pessimistic Locking in a transaction, and access an entity, it’ll be locked immediately. The transaction releases the lock either by committing or rolling back the transaction.
2.2. Optimistic Locking
In Optimistic Locking, the transaction doesn’t lock the entity immediately. Instead, the transaction commonly saves the entity’s state with a version number assigned to it.
When we try to update the entity’s state in a different transaction, the transaction compares the saved version number with the existing version number during the update.
At this point, if the version number differs, it means that we can’t modify the entity. If there’s an active transaction, then that transaction will be rolled back and the underlying JPA implementation will throw an OptimisticLockException.
Depending on what’s most suitable for our current development context, we can also use other approaches, such as timestamps, hash value computation, or serialized checksum.
3. Enabling Transaction Locks on Query Methods
To acquire a lock on an entity, we can annotate the target query method with a Lock annotation by passing the required lock mode type.
Lock mode types are enum values to be specified while locking an entity. The specified lock mode is then propagated to the database to apply the corresponding lock on the entity object.
To specify a lock on a custom query method of a Spring Data JPA repository, we can annotate the method with @Lock and specify the required lock mode type:
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
@Query("SELECT c FROM Customer c WHERE c.orgId = ?1")
public List<Customer> fetchCustomersByOrgId(Long orgId);
To enforce the lock on predefined repository methods, such as findAll or findById(id), we have to declare the method within the repository and annotate the method with the Lock annotation:
@Lock(LockModeType.PESSIMISTIC_READ)
public Optional<Customer> findById(Long customerId);
When the lock is explicitly enabled, and there’s no active transaction, the underlying JPA implementation will throw a TransactionRequiredException.
If the lock can’t be granted, and the locking conflict doesn’t result in a transaction rollback, JPA throws a LockTimeoutException, but it doesn’t mark the active transaction for rollback.
4. Setting Transaction Lock Timeouts
When using Pessimistic Locking, the database will try to lock the entity immediately. The underlying JPA implementation throws a LockTimeoutException when the lock can’t be obtained immediately. To avoid such exceptions, we can specify the lock timeout value.
In Spring Data JPA, the lock timeout can be specified using the QueryHints annotation by placing a QueryHint on query methods:
@Lock(LockModeType.PESSIMISTIC_READ)
@QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")})
public Optional<Customer> findById(Long customerId);
We can find further details on setting the lock timeout hint at different scopes in this ObjectDB article.
5. Conclusion
In this article, we explored the different types of transaction lock modes. Then we learned how to enable transaction locks in Spring Data JPA. We also covered setting lock timeouts.
Applying the right transaction locks at the right places can help to maintain data integrity in high-volume concurrent usage applications.
When the transaction needs to strictly adhere to ACID rules, we should use Pessimistic Locking. Optimistic Locking should be applied when we need to allow multiple concurrent reads and when eventual consistency is acceptable within the application context.
As always, the sample code for both Pessimistic Locking and Optimistic Locking can be found over on Github.