You can propagate changes from child entities to parent entities. This requires you to propagate the OPTIMISTIC_FORCE_INCREMENT lock whenever the child entity is modified.
So, you need to have all your entities implementing a 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) { //Do nothing } }
and then supply the RootAwareFlushEntityEventListenerIntegrator via a Hibernate configuration property:
configuration.put( "hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList( RootAwareEventListenerIntegrator.INSTANCE ) );
Now, when you modify 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);
The parent Post entity version is modified as well:
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