Consider two entities Parent and Child.
I just implemented something similar which is working like beast fast and nice. At the moment you are saving your 'child' just call do something like:
save(child);
T parent = child.getParentEntity();
entityManager.lock(parent, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
You need access to the entity manager which can be obtained in spring like:
@PersistenceContext
private EntityManager entityManager;
Your parent entity should have @Version from javax.persistence.Version and NOT the spring one. (I assume that on the child save moment you will have all the verifications and things done so when you are saving the child the parent should get dirty for sure)
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
I think I figured it out. After merge is called an attached instance reference is returned. When I obtain an explicit lock for that using entityManager.lock(updated, LockModeType.WRITE); then version number is increased even if Parent instance was not updated in db.
In addition I am comparing detached instance version with persisted instance version. If they don't match then Parent was updated in db and also version number has changed. This keeps version numbers consistent. Otherwise entityManager.lock would increase version number even if merge operation changed it.
Still looking for solution how to make hibernate increase version when entity is not dirty during merge.
i don't think you can force hibernate to increase a version number for a non-changed object, because it will just not do any db UPDATE
query if nothing has changed (for obvious reasons).
you could do a nasty hack like adding a new field to the object and incrementing that manually, but personally that seems to be a waste of time and resources. i'd go with your explicit locking solution since that seems to give you what you want, without unnecessary hackery.