You can propagate changes from child entities to parent entities. This requires that you extend the OPTIMISTIC_FORCE_INCREMENT lock whenever the child entity changes.
This article explains in detail how you should implement this use case.
In short, you need all your entities to implement the RootAware interface:
public interface RootAware<T> { T root(); } @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; @ManyToOne(fetch = FetchType.LAZY) @MapsId private PostComment comment; private int votes; //Getters and setters omitted for brevity @Override public Post root() { return comment.getPost(); } }
Then you need two event listeners:
public static 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); } }
and
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; } }
which you can register as follows:
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) {
and then RootAwareFlushEntityEventListenerIntegrator via the Hibernate configuration property:
configuration.put( "hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList( RootAwareEventListenerIntegrator.INSTANCE ) );
Now, when you change the essence of PostCommentDetails :
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);
The version of the parent entity Post also been changed:
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