How to increment the parent entity version whenever a child entity gets modified with JPA and Hibernate

Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. Hypersistence Optimizer is that tool!

Introduction

StackOverflow and the Hibernate forum are gold mines. Yesterday, I bumped on the following question on our forum:

Usually, the rationale behind clustering objects together is to form a transactional boundary inside which business invariants are protected. I’ve noticed that with the OPTIMISTIC locking mode changes to a child entity will not cause a version increment on the root. This behavior makes it quite useless to cluster objects together in the first place.

Is there a way to configure Hibernate so that any changes to an object cluster will cause the root object’s version to increment? I’ve read about OPTIMISTIC_FORCE_INCREMENT but I think this does increment the version regardless of if entities were changed or not. Since reads shouldn’t be conflicting with other reads in most scenarios, this doesn’t seem so useful either.

I could always increment the version inside every mutating behavior of the root, but that is quite error-prone. I’ve also thought of perhaps using AOP to do this, but before looking into it, I wanted to know if there were any easy way to do that. If there were a way to check if an object graph is dirty, then it would make it quite easy to implement as well.

What a brilliant question! This post is going to demonstrate how easy you can implement such a requirement when using Hibernate.

Domain Model

First, let’s assume we have the following entities in our system:

Domain Model Entities

The Post is the root entity, and it might have several PostComment entities. Every PostComment can have at most one PostCommentDetails. These entities are mapped as follows:

@Entity(name = "Post") 
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    @Version
    private int version;

    //Getters and setters omitted for brevity
}

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment 
    implements RootAware<Post> {

    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    private String review;

    //Getters and setters omitted for brevity

    @Override
    public Post root() {
        return post;
    }
}

@Entity(name = "PostCommentDetails")
@Table(name = "post_comment_details")
public class PostCommentDetails 
    implements RootAware<Post> {

    @Id
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private PostComment comment;

    private int votes;

    //Getters and setters omitted for brevity

    @Override
    public Post root() {
        return comment.root();
    }
}

As you probably noticed, the @OneToOne association uses the awesome @MapsId mapping which I already explained in this post.

The PostComment and PostCommentDetails entities are implementing the RootAware interface which is very straightforward:

public interface RootAware<T> {
    T root();
}

By implementing the RootAware interface, we can resolve the root entity for any PostComment and PostCommentDetails entity.

Event Listeners

Contrary to popular belief, Hibernate is not just an ORM framework but a very customizable data access platform. For our example, we need to intercept any child entity modification and acquire an OPTIMISTIC_FORCE_INCREMENT event on the associated root entity.

To intercept the UPDATE and the DELETE SQL events, the following custom entity event listener is needed:

public class RootAwareUpdateAndDeleteEventListener 
    implements FlushEntityEventListener {

    private static final Logger LOGGER = 
        LoggerFactory.getLogger(RootAwareUpdateAndDeleteEventListener.class);

    public static final RootAwareUpdateAndDeleteEventListener INSTANCE = 
        new RootAwareUpdateAndDeleteEventListener();

    @Override
    public void onFlushEntity(FlushEntityEvent event) throws HibernateException {
        final EntityEntry entry = event.getEntityEntry();
        final Object entity = event.getEntity();
        final boolean mightBeDirty = entry.requiresDirtyCheck( entity );

        if(mightBeDirty && entity instanceof RootAware) {
            RootAware rootAware = (RootAware) entity;
            if(updated(event)) {
                Object root = rootAware.root();
                LOGGER.info("Incrementing {} entity version because a {} child entity has been updated", 
                    root, entity);
                incrementRootVersion(event, root);
            }
            else if (deleted(event)) {
                Object root = rootAware.root();
                LOGGER.info("Incrementing {} entity version because a {} child entity has been deleted", 
                    root, entity);
                incrementRootVersion(event, root);
            }
        }
    }

    private void incrementRootVersion(FlushEntityEvent event, Object root) {
        event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT);
    }

    private boolean deleted(FlushEntityEvent event) {
        return event.getEntityEntry().getStatus() == Status.DELETED;
    }

    private boolean updated(FlushEntityEvent event) {
        final EntityEntry entry = event.getEntityEntry();
        final Object entity = event.getEntity();

        int[] dirtyProperties;
        EntityPersister persister = entry.getPersister();
        final Object[] values = event.getPropertyValues();
        SessionImplementor session = event.getSession();

        if ( event.hasDatabaseSnapshot() ) {
            dirtyProperties = persister.findModified( 
                event.getDatabaseSnapshot(), values, entity, session 
            );
        }
        else {
            dirtyProperties = persister.findDirty( 
                values, entry.getLoadedState(), entity, session 
            );
        }

        return dirtyProperties != null;
    }
}

This event listener is going to be executed whenever an entity is flushed by the currently running Persistence Context. Every entity modification is automatically detected by the dirty checking mechanism and marked as dirty.

If the entity is dirty and implements the RootAware interface, then we can just lock the parent entity with an OPTIMISTIC_FORCE_INCREMENT lock type. This lock type is going to increment the root entity version during the flush operation.

To intercept when new child entities are being persisted, the following event listener is needed:

public class RootAwareInsertEventListener 
    implements PersistEventListener {

    private static final Logger LOGGER = 
        LoggerFactory.getLogger(RootAwareInsertEventListener.class);

    public static final RootAwareInsertEventListener INSTANCE = 
        new RootAwareInsertEventListener();

    @Override
    public void onPersist(PersistEvent event) throws HibernateException {
        final Object entity = event.getObject();

        if(entity instanceof RootAware) {
            RootAware rootAware = (RootAware) entity;
            Object root = rootAware.root();
            event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT);

            LOGGER.info("Incrementing {} entity version because a {} child entity has been inserted", 
                root, entity);
        }
    }

    @Override
    public void onPersist(PersistEvent event, Map createdAlready) 
        throws HibernateException {
        onPersist(event);
    }
}

To register these two event listeners, we need to provide a org.hibernate.integrator.spi.Integrator implementation:

public class RootAwareEventListenerIntegrator
    implements org.hibernate.integrator.spi.Integrator {

    public static final RootAwareEventListenerIntegrator INSTANCE = 
        new RootAwareEventListenerIntegrator();

    @Override
    public void integrate(
            Metadata metadata,
            SessionFactoryImplementor sessionFactory,
            SessionFactoryServiceRegistry serviceRegistry) {

        final EventListenerRegistry eventListenerRegistry =
                serviceRegistry.getService( 
                    EventListenerRegistry.class 
        );

        eventListenerRegistry.appendListeners(
            EventType.PERSIST, 
            RootAwareInsertEventListener.INSTANCE
        );
        eventListenerRegistry.appendListeners(
            EventType.FLUSH_ENTITY, 
            RootAwareUpdateAndDeleteEventListener.INSTANCE
        );
    }

    @Override
    public void disintegrate(
            SessionFactoryImplementor sessionFactory,
            SessionFactoryServiceRegistry serviceRegistry) {
        //Do nothing
    }
}

When bootstrapping the JPA EntityManagerFactory, we can provide the RootAwareEventListenerIntegrator via the hibernate.integrator_provider configuration property:

configuration.put(
    "hibernate.integrator_provider", 
    (IntegratorProvider) () -> Collections.singletonList(
        RootAwareEventListenerIntegrator.INSTANCE
    )
);

To see how you can set the hibernate.integrator_provider configuration property when using Spring with JPA or Spring with Hibernate, check out this article.

Testing time

Assuming we have the following entities within our system:

doInJPA(entityManager -> {
    Post post = new Post();
    post.setId(1L);
    post.setTitle("High-Performance Java Persistence");

    PostComment comment1 = new PostComment();
    comment1.setId(1L);
    comment1.setReview("Good");
    comment1.setPost(post);

    PostCommentDetails details1 = new PostCommentDetails();
    details1.setComment(comment1);
    details1.setVotes(10);

    PostComment comment2 = new PostComment();
    comment2.setId(2L);
    comment2.setReview("Excellent");
    comment2.setPost(post);

    PostCommentDetails details2 = new PostCommentDetails();
    details2.setComment(comment2);
    details2.setVotes(10);

    entityManager.persist(post);
    entityManager.persist(comment1);
    entityManager.persist(comment2);
    entityManager.persist(details1);
    entityManager.persist(details2);
});

Updating child entities

When updating a PostCommentDetails entity:

PostCommentDetails postCommentDetails = entityManager.createQuery(
    "select pcd " +
    "from PostCommentDetails pcd " +
    "join fetch pcd.comment pc " +
    "join fetch pc.post p " +
    "where pcd.id = :id", PostCommentDetails.class)
.setParameter("id", 2L)
.getSingleResult();

postCommentDetails.setVotes(15);

Hibernate generates the following SQL statements:

SELECT  pcd.comment_id AS comment_2_2_0_ ,
        pc.id AS id1_1_1_ ,
        p.id AS id1_0_2_ ,
        pcd.votes AS votes1_2_0_ ,
        pc.post_id AS post_id3_1_1_ ,
        pc.review AS review2_1_1_ ,
        p.title AS title2_0_2_ ,
        p.version AS version3_0_2_
FROM    post_comment_details pcd
INNER JOIN post_comment pc ON pcd.comment_id = pc.id
INNER JOIN post p ON pc.post_id = p.id
WHERE   pcd.comment_id = 2

UPDATE post_comment_details 
SET votes = 15 
WHERE comment_id = 2

UPDATE post 
SET version = 1 
where id = 1 AND version = 0

As you can see, not only the post_comment_details row gets updated but the post version is also incremented.

The same goes for the PostComment entity modifications:

PostComment postComment = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "join fetch pc.post p " +
    "where pc.id = :id", PostComment.class)
.setParameter("id", 2L)
.getSingleResult();

postComment.setReview("Brilliant!");

Hibernate generating the following SQL statements:

SELECT  pc.id AS id1_1_0_ ,
        p.id AS id1_0_1_ ,
        pc.post_id AS post_id3_1_0_ ,
        pc.review AS review2_1_0_ ,
        p.title AS title2_0_1_ ,
        p.version AS version3_0_1_
FROM    post_comment pc
INNER JOIN post p ON pc.post_id = p.id
WHERE   pc.id = 2

UPDATE post_comment 
SET post_id = 1, review = 'Brilliant!' 
WHERE id = 2

UPDATE post 
SET version = 2 
WHERE id = 1 AND version = 1

Adding new child entities

The parent Post entity version is incremented even when a new child entity is being persisted:

Post post = entityManager.getReference(Post.class, 1L);

PostComment postComment = new PostComment();
postComment.setId(3L);
postComment.setReview("Worth it!");
postComment.setPost(post);
entityManager.persist(postComment);

Hibernate generates the following SQL statements:

SELECT p.id AS id1_0_0_ ,
       p.title AS title2_0_0_ ,
       p.version AS version3_0_0_
FROM   post p
WHERE  p.id = 1

INSERT INTO post_comment (post_id, review, id) 
VALUES (1, 'Worth it!', 3)

UPDATE post 
SET version = 3 
WHERE id = 1 AND version = 2

Removing child entities

This solution works even when removing existing child entities:

PostComment postComment = entityManager.getReference(PostComment.class, 3l);
entityManager.remove(postComment);

Hibernate being able to increment the parent entity version accordingly:

SELECT pc.id AS id1_1_0_ ,
       pc.post_id AS post_id3_1_0_ ,
       pc.review AS review2_1_0_
FROM   post_comment pc
WHERE  pc.id = 3

SELECT p.id AS id1_0_0_ ,
       p.title AS title2_0_0_ ,
       p.version AS version3_0_0_
FROM   post p
WHERE  p.id = 1

DELETE FROM post_comment 
WHERE id = 3

UPDATE post 
SET version = 4 
WHERE id = 1 and version = 3

Cool, right?

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

Conclusion

Synchronizing a root entity version for every child entity modification is fairly easy with Hibernate. Hibernate supports many concurrency control mechanisms, as illustrated in this comprehensive tutorial.

Code available on GitHub.

FREE EBOOK

29 Comments on “How to increment the parent entity version whenever a child entity gets modified with JPA and Hibernate

  1. These changes are always executed in the context of a database transaction. Hibernate cannot modify data without an explicit transaction.

  2. Does this work with different EntityManagers?

    open DB
    In my application I read a Customer
    Close DB

    UI waits for user input

    open DB
    Adds a child object
    persists data
    Close DB

    This method doesn’t seem to work with different entity managers?

      • Send a Pull Request that proves it so I can take a look.

    • Of course it works. That’s the whole point of optimistic locking.

  3. The above seems to cause updates when we make queries and do not update the entities as part of a transaction. Seems the commit of the transaction is causing a flush event cascading down to a persist even though nor the parent or child entities have been modified.

    • Send me a Pull Request with a replicating test case so that I can see it too. The High-Performance Java Persistence GitHub repository already contains the test case in this article, so you just have to modify it to prove that the parent entity is modified even without modifying the child entities.

  4. This is a nice description. I have a question.
    For example what if we have an address entity and multiple other entities (employee, shop, partner) can have an address. The address entity don’t know about the root entity. However if the address changes I want to update root entity version.

    • The address is the root in your example since the other entities reference it. Hence, it’s just the same as in this example.

      • Hello Vlad,

        Maybe I have missed something.

        Let’s suppose I have an Employee entity that has an Address entity. The Address entity has no reference to any other entity. As you wrote the address entity is the root. This is technically true only as business point of view the Employee is the root.

        Anyway, if the Employee is a RootAware and has the root() method it doesn’t help if only address is changed (the Employee is not dirty).

        If the Address is RootAware and set the Employee as root then the version update will be called on the Employee as expected (theoretically). But, in this case I need to store the Employee in the address entity and I should make this field Transient. However in the listener method the transient fields are not more available so root() returns null.

      • Then, don’t use the RootAware mechanism and define that logic in the event listener, using either the entity properties or queries to fetch the entities you need to increase the version.

  5. Hi Vlad

    Like Xavier, I am trying to get the root object version change into the root_aud table.

    I have tried to create a Root.versionChanged property, and in the 2 listeners (RootAwareInsertEventListener and RootAwareUpdateAndDeleteEventListener), I then call rootAware.rootVersionChanged, which then call root.setVersionChanged(), but it seems that the change (in root_aud table and in revchanges) only get registrered in the RootAwareInsertEventListener case, do you know what prevents this from being changed in the RootAwareUpdateAndDeleteEventListener ?

    Here is the code where I try to make the change:

    private void incrementRootVersion(FlushEntityEvent event, Object root, RootAware rootAware) {
    event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT);
    rootAware.rootVersionChanged();
    }

    Thanks.

    • It’s because the UpdateAndDeleteListenr is called in the post-flush phase, therefore no change will be tracked since the dirtyProperties array was already calculated. Use Envers if you need an audit logging mechanism.

      • I am using Envers – but as Xavier describes: “My problem is when a child entity is modified, a new revision is only created for the child but not for the root.”

        See: https://hibernate.atlassian.net/browse/HHH-11857

        I am trying to solve this by adding another field “versionChanged” on the root-object, but I guess I need to then set that when a child object is created/updated/deleted manually.

      • You could use the Hibernate Interceptor and add your logic in either preFlush or findDirty if you want more control.

    • I had the same problem. I wanted the owning entity’s “lastModifiedDate” and “Version” fields update when the child entities change. I have tested so many things and implemented lots of custom code. Even I was trying to implement a custom dirty check mechanism. nothing worked for auditing. Finally, Using Hibernate Bytecode Enhancement Maven plugin and enabling DirtyTracking fixed the issue of parent versioning and auditing for me. Now the owning entity’s “lastModifiedDate” and “version” updates by itself and without any custom code when I edit the child. Bear in mind that I have only tested ManyToOne/OneToMany relation but I think this should work for other relations as well. Also, I am updating the entity from the owning side (using Spring Data)

  6. Looks great.

    How do I add the “hibernate.integrator_provider” direcly in the jpa persistence.xml file ?

      • Can you give an example on what the “value” attribute should be set to ?

      • https://github.com/hibernate/hibernate-orm/blob/master/hibernate-core/src/main/java/org/hibernate/integrator/internal/IntegratorServiceImpl.java

        It looks like only the 3 integrators from IntegratorServiceImpl are added, not the one provided via the property “hibernate.integrator_provider” – so something must be wrong:

        DEBUG 2019-02-28 15:42:35,027 [main] org.hibernate.integrator.internal.IntegratorServiceImpl: Adding Integrator [org.hibernate.cfg.beanvalidation.BeanValidationIntegrator].
        DEBUG 2019-02-28 15:42:35,029 [main] org.hibernate.integrator.internal.IntegratorServiceImpl: Adding Integrator [org.hibernate.secure.spi.JaccIntegrator].
        DEBUG 2019-02-28 15:42:35,032 [main] org.hibernate.integrator.internal.IntegratorServiceImpl: Adding Integrator [org.hibernate.cache.internal.CollectionCacheInvalidator].
        INFO 2019-02-28 15:42:35,120 [main] org.hibernate.Version: HHH000412: Hibernate Core {5.4.1.Final}

        I have set “some.package.name.RootAwareEventListenerIntegrator” as the property value.

      • You’re right. That property takes an IntegratorProvider Java object only. You need to add a Jira issue for that and assign it to me.

      • Great – where should I create the jira-issue ? (Do you have a link?)

  7. Before I found this article we marked a parent entity as dirty by changing a modified field (filled by a JPA-Listener), and after a flush the parent entity has its new version (unless the JPA-Listener inserts the same value back).

    Using this post’s method I run into the same issue as Kristina, but flushing the EntityManager doesn’t update the parent’s version field until the tx is committed.

    I much prefer this posts method, especially since changing a modified field can result in a missing version bump if the JPA-Listener inserts the same value again, but I need the new parent version after a flush.

    Is there any other way to achieve this?

    • Try to replicate it with a test case. You can use the test on GitHub as a template to emulate your use case which you say is not working.

  8. Very nice solution.

    The use of
    event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT);
    will result in an additional update statement even for a dirty entity. In my application, I have a need to update an entity’s version regardless of whether or not its dirty. With this in mind, if I use LockMode.OPTIMISTIC two update statements will be issued (one for the actual change and one for the lock mode). Alternatively I could just increment the version programmatically (entity.setVersion(entity.getVersion()++)). Is there anything wrong with that approach?

    Assuming that a version update is needed on an entity and its unknown if the entity has changed

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.