Spring Data JPA entity locking

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

In this article, we are going to see what options Spring Data JPA offers for entity locking.

We will see how we can apply a shared or exclusive row-level lock upon fetching one or multiple entities.

JPA LockModeType

As I explained in this article, JPA offers several explicit locking options that can be applied at entity fetching time via the LockModeType enumeration.

If you want to apply a pessimistic lock, then you can use one of the following options:

  • LockModeType.PESSIMISTIC_READ to apply a shared or read lock on the associated table records.
  • LockModeType.PESSIMISTIC_WRITE to apply an exclusive or write lock on the associated table records.

If the underlying database does not support shared locks, then the PESSIMISTIC_READstrategy will fall back to a PESSIMISTIC_WRITE since this option is widely supported.

Direct entity locking

If we want to lock an entity upon fetching it, then the JPA EntityManager offers a find method variant that allows us to pass the LockModeType:

Post post = entityManager.find(Post.class, id, lockMode);

However, while the Spring Data JpaRepository doesn’t offer this option, we can easily add it using a custom Spring Data Repository.

For instance, we can have our PostRepository extend a CustomPostRepository beside the JpaRepository:

@Repository
public interface PostRepository 
        extends JpaRepository<Post, Long>, CustomPostRepository {
}

In the CustomPostRepository interface, we can define a lockById method like this:

public interface CustomPostRepository {

    Post lockById(Long id, LockModeType lockMode);
}

While Spring Data JPA can provide an implementation for query methods or methods annotated with the @Query annotation, for our CustomPostRepository interface, we will have to provide the CustomPostRepositoryImpl implementation that Spring Data JPA will use when creating the PostRepository Java object instance at runtime.

The CustomPostRepositoryImpl looks as follows:

public class CustomPostRepositoryImpl 
        implements CustomPostRepository {
    
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Post lockById(Long id, LockModeType lockMode) {
        return entityManager.find(Post.class, id, lockMode);
    }
}

Having the implementation of the lockById method in place, we can now test it.

To apply a shared lock on PostgreSQL, we will have to use the LockModeType.PESSIMISTIC_READ strategy, like this:

Post postWithSharedLock = postRepository.lockById(
    1L, 
    LockModeType.PESSIMISTIC_READ
);

And, Hibernate will execute the following SQL query:

SELECT 
    p.id AS id1_0_0_,
    p.slug AS slug2_0_0_,
    p.title AS title3_0_0_
FROM 
    post p
WHERE 
    p.id = 1
FOR SHARE

To acquire an exclusive lock, we can pass the PESSIMISTIC_WRITE option:

Post postWithExclusiveLock = postRepository.lockById(
    2L, 
    LockModeType.PESSIMISTIC_WRITE
);

And, Hibernate will use a FOR UPDATE clause instead:

SELECT 
    p.id AS id1_0_0_,
    p.slug AS slug2_0_0_,
    p.title AS title3_0_0_
FROM 
    post p
WHERE 
    p.id = 2
FOR UPDATE

Query entity locking

We can also apply row-level locks to all the records returned by a given SQL query, and for that, Spring Data JPA offers the @Lock annotation.

For instance, in the PostCommentRepository, we can define a lockAllByPostId method that applies a read lock on all the child PostComment rows that are associated with a given Post parent record:

@Repository
public interface PostCommentRepository 
        extends JpaRepository<PostComment, Long> {

    @Query("""
        select pc
        from PostComment pc
        where pc.post.id = :postId
        """)
    @Lock(LockModeType.PESSIMISTIC_READ)
    List<PostComment> lockAllByPostId(
        @Param("postId") Long postId
    );
}

And when executing the lockAllByPostId method:

List<PostComment> commentWithLock = postCommentRepository.lockAllByPostId(1L);

Hibernate will run the following SQL query on PostgreSQL:

SELECT 
    pc.id as id1_1_, 
    pc.post_id as post_id3_1_, 
    pc.review as review2_1_ 
FROM 
    post_comment pc 
WHERE 
    pc.post_id = 1
FOR SHARE OF pc

Cool, right?

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

Locking entities is very easy with Spring Data JPA, and it works for both direct entity fetching as it does for queries.

In fact, I think it would be a good idea to add the lockById method to the BaseJpaRepository utility offered by the Hypersistence Utils project.

Transactions and Concurrency Control eBook

One Comment on “Spring Data JPA entity locking

  1. How about explicit optimistic locking in Spring Data JPA?

    There are many types of explicit locking options. But, passing the LockModeType is the same.

    Previously, you said there is always a risk of optimistic locking race conditions (between the version check and committing the transaction).

    I suppose you are talking about the article I wrote about LockModeType.OPTIMISTIC.

    Does it mean that if we want to apply an explicit optimistic locking upon entity fetching in the same manner as it was shown in this article, then we might do need some additional functionality (such as mentioned in the first comment – adding a separate lock method), so that we can use it to set a LockModeType.PESSIMISTIC_READ at the end of each transaction where we use an entity record previously fetched with explicit optimistic locking?

    For LockModeType.OPTIMISTIC, you’d want to acquire a shared lock on the entity you passed the LockModeType.OPTIMISTIC prior to existing your method. For other explicit lock mode options, like OPTIMISTIC_FORCE_INCREMENT,you don’t need to do that.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.