Consider two entities Parent and Child.
Both are displayed on the same form to a user. When user saves the data we first update Parent instance and then Child collection (both using merge).
Now comes the tricky part. When user modifies only Child property on the form then hibernate dirty checking does not update Parent instance and thus does not increase optimistic locking version number for that entity.
I would like to see situation where only Parent is versioned and every time I call merge for Parent then version is always updated even if actual update is not executed in db.
My parent class : @Entity @DynamicUpdate @Table(name = "person") public class Person { @Id @GeneratedValue(strategy = GenerationType. IDENTITY) @Column(name="person_pk", nullable=false) private Long personPK; @OneToMany(fetch = FetchType. EAGER, mappedBy = "person",cascade = CascadeType.
When ever we use versioning then hibernate inserts version number as zero, when ever object is saved for the first time in the database. Later hibernate increments that version no by one automatically when ever a modification is done on that particular object.
If the data is read by two threads at the same time, and both try to update the same row with different values, Hibernate uses the @Version field to check if the row is already updated. Before committing, each transaction verifies that no other transaction has modified its data.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With