Enterprise Java

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:

postcommentdetailslockcascade

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.

Reference: Hibernate CascadeType.LOCK gotchas from our JCG partner Vlad Mihalcea at the Vlad Mihalcea’s Blog blog.

Vlad Mihalcea

Vlad Mihalcea is a software architect passionate about software integration, high scalability and concurrency challenges.
Subscribe
Notify of
guest

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

0 Comments
Inline Feedbacks
View all comments
Back to top button