Hibernate Parent Version Control

Consider two objects, Parent and Child.

  • The child is part of the Parent Transient Collection.
  • The child has a ManyToOne mapping to the parent with FetchType.LAZY

Both are displayed in the same form to the user. When the user saves the data, we first update the parent instance and then the child collection (both use merge).

Now comes the hard part. When the user changes only the Child property on the form, then the validation sleep mode does not update the parent instance and thus does not increase the optimistic lock version number for this object.

I would like to see a situation where only Parent is versioned, and every time I call merge for Parent, then the version is always updated, even if the actual update is not performed in db.

+4
source share
4 answers

I think I figured it out. After the merge is called a reference, the reference to the instance is returned. When I get an explicit lock for this using entityManager.lock (updated, LockModeType.WRITE); then the version number is incremented even if the parent instance was not updated in db.

In addition, I am comparing a single instance version with a constant instance version. If they do not match, then Parent has been updated in db, and the version number has also changed. This allows you to save version numbers. Otherwise, entityManager.lock would increment the version number even if the merge operation changed it.

We are looking for a solution on how to make the hibernation version larger when the entity is not polluted during the merge.

+3
source

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) { //Do nothing } } 

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 
+2
source

I do not think that you can force hibernate to increase the version number for an immutable object, because it simply will not make any db UPDATE request if nothing changes (for obvious reasons).

you can make it unpleasant to hack, like adding a new field to an object and increasing it manually, but personally it seems like a waste of time and resources. I would go with your explicit solution to blocking, as this seems to give you what you want without the hacker.

0
source

I just implemented something similar that works like a beast quickly and beautifully. At the moment, you save your "child", just call and do something like:

 save(child); T parent = child.getParentEntity(); entityManager.lock(parent, LockModeType.OPTIMISTIC_FORCE_INCREMENT); 

You need access to the entity manager that spring can get, for example:

  @PersistenceContext private EntityManager entityManager; 

Your parent entity should have @Version from javax.persistence.Version and NOT spring. (I assume that at the time of saving the child you will have all the checks and everything that is needed, therefore, when you save the child, the parent must necessarily get dirty)

0
source

Source: https://habr.com/ru/post/1310713/


All Articles