Consider two entities Parent and Child.
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 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 {
@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 {
@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