JPA can behave very differently depending on the exact circumstances under which it is used. Code that works in our local environment or in staging performs very poorly (or even flat out fails) when thrown against real-scale databases in production environments.

Debugging these JPA issues in production is pretty difficult - existing APMs don’t provide enough granular insights at the code level, and tracking every single place someone queried entities one by one instead of in bulk can be a grueling, time-consuming task.

Lightrun is a new approach to debugging in production. Using Lightrun’s Logs and Snapshots, you can now get debugger-level granularity in production without opening inbound ports, redeploying, restarting, or even stropping the running application.

In addition, instrumenting Lightrun Metrics at runtime allows you to track down persistence issues securely and in real-time. Want to see it in action? Check out our 2-minute tutorial for debugging JPA performance issues in production using Lightrun:

>> Debugging Spring Persistence and JPA Issues Using Lightrun

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 will also have a look at different lock types and setting transaction lock timeouts.

2. Lock Types

JPA has two main lock types defined, which are Pessimistic Locking and Optimistic Locking.

2.1. Pessimistic Locking

When we are using Pessimistic Locking in a transaction and access an entity, it will 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 an update.

At this point, if the version number differs, it means that the entity can't be modified. If there is an active transaction then that transaction will be rolled back and the underlying JPA implementation will throw an OptimisticLockException.

Apart from the version number approach, we can use other approaches such as timestamps, hash value computation, or serialized checksum, depending on which approach is the most suitable for our current development context.

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 is no active transaction, the underlying JPA implementation will throw a TransactionRequiredException.

In case the lock cannot 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 cannot 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 = "javax.persistence.lock.timeout", value = "3000")})
public Optional<Customer> findById(Long customerId);

Further details on setting the lock timeout hint at different scopes can be found in this ObjectDB article.

5. Conclusion

In this tutorial, we've learned the different types of transaction lock modes. We've learned how to enable transaction locks in Spring Data JPA. We've 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 adhere to ACID rules strictly, 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.

Of course, the sample code for both Pessimistic Locking and Optimistic Locking can be found over on Github.

Persistence bottom
Get started with Spring Data JPA through the reference Learn Spring Data JPA course: >> CHECK OUT THE COURSE
Persistence footer banner
Comments are closed on this article!