Hibernate CascadeType.LOCK gotchas
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", optional = false, 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
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.
- Code available on GitHub.
Reference: | Hibernate CascadeType.LOCK gotchas from our JCG partner Vlad Mihalcea at the Vlad Mihalcea’s Blog blog. |