How does CascadeType.LOCK works in JPA and Hibernate
Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?
What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?
Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, Micronaut, or Play Framework.
So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!
Introduction
Having introduced Hibernate explicit locking support, as well as Cascade Types, it’s time to analyze the CascadeType.LOCK behavior.
A Hibernate lock request triggers an internal LockEvent. The associated DefaultLockEventListener may cascade the lock request to the locking entity children.
Since CascadeType.ALL includes CascadeType.LOCK too, it’s worth understanding when a lock request propagates from a Parent to a Child entity.
Testing time
We’ll start with the following entity model:
The Post is the Parent entity of both the PostDetail one-to-one association and the Comment one-to-many relationship, and these associations are marked with CascadeType.ALL:
@OneToMany(
cascade = CascadeType.ALL,
mappedBy = "post",
orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
@OneToOne(
cascade = CascadeType.ALL,
mappedBy = "post",
fetch = FetchType.LAZY)
private PostDetails details;
All the up-coming test cases will use the following entity model graph:
doInTransaction(session -> {
Post post = new Post();
post.setName("Hibernate Master Class");
post.addDetails(new PostDetails());
post.addComment(new Comment("Good post!"));
post.addComment(new Comment("Nice post!"));
session.persist(post);
});
Locking managed entities
A managed entity is loaded in the current running Persistence Context and all entity state changes are translated to DML statements.
When a managed Parent entity is being locked:
doInTransaction(session -> {
Post post = (Post) session.createQuery(
"select p " +
"from Post p " +
"join fetch p.details " +
"where " +
" p.id = :id")
.setParameter("id", 1L)
.uniqueResult();
session.buildLockRequest(
new LockOptions(LockMode.PESSIMISTIC_WRITE))
.lock(post);
});
Only the Parent entity gets locked, the cascade being therefore prevented:
select id from Post where id = 1 for update
Hibernate defines a scope LockOption, which (according to JavaDocs) should allow a lock request to be propagated to Child entities:
“scope” is a JPA defined term. It is basically a cascading of the lock to associations.
session.buildLockRequest(
new LockOptions(LockMode.PESSIMISTIC_WRITE))
.setScope(true)
.lock(post);
Setting the scope flag doesn’t change anything, only the managed entity being locked:
select id from Post where id = 1 for update
Locking detached entities
Apart from entity locking, the lock request can reassociate detached entities too. To prove this, we are going to check the Post entity graph before and after the lock entity request:
void containsPost(Session session,
Post post, boolean expected) {
assertEquals(expected,
session.contains(post));
assertEquals(expected,
session.contains(post.getDetails()));
for(Comment comment : post.getComments()) {
assertEquals(expected,
session.contains(comment));
}
}
The following test demonstrates how CascadeType.LOCK works for detached entities:
//Load the Post entity, which will become detached
Post post = doInTransaction(session ->
(Post) session.createQuery(
"select p " +
"from Post p " +
"join fetch p.details " +
"join fetch p.comments " +
"where " +
" p.id = :id")
.setParameter("id", 1L)
.uniqueResult());
//Change the detached entity state
post.setName("Hibernate Training");
doInTransaction(session -> {
//The Post entity graph is detached
containsPost(session, post, false);
//The Lock request associates
//the entity graph and locks the requested entity
session.buildLockRequest(
new LockOptions(LockMode.PESSIMISTIC_WRITE))
.lock(post);
//Hibernate doesn't know if the entity is dirty
assertEquals("Hibernate Training",
post.getName());
//The Post entity graph is attached
containsPost(session, post, true);
});
doInTransaction(session -> {
//The detached Post entity changes have been lost
Post _post = (Post) session.get(Post.class, 1L);
assertEquals("Hibernate Master Class",
_post.getName());
});
The lock request reassociates the entity graph, but the current running Hibernate Session is unaware the entity became dirty, while in detached state. The entity is just reattached without forcing an UPDATE or selecting the current database state for further comparison.
Once the entity becomes managed, any further change will be detected by the dirty checking mechanism and the flush will propagate the ante-reattachment changes as well. If no change happens while the entity is managed, the entity will not be scheduled for flushing.
If we want to make sure, the detached entity state is always synchronized with the database, we need to use merge or update.
The detached entities propagate the lock options, when the scope option is set to true:
session.buildLockRequest(
new LockOptions(LockMode.PESSIMISTIC_WRITE))
.setScope(true)
.lock(post);
The Post entity lock event is propagated to all Child entities (since we are using CascadeType.ALL):
select id from Comment where id = 1 for update select id from Comment where id = 2 for update select id from PostDetails where id = 1 for update select id from Post where id = 1 for update
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
The lock cascading is far from being straight-forward or intuitive. Explicit locking requires diligence (the more locks we acquire, the greater the chance of dead-locking) and you are better off retaining full-control over Child entity lock propagation anyway. Analogous to concurrency programming best practices, manual locking is therefore preferred over automatic lock propagation.



