Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hibernate versioning parent entity

Consider two entities Parent and Child.

  • Child is part of Parent's transient collection
  • Child has a ManyToOne mapping to parent with FetchType.LAZY

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.

like image 437
Priit Avatar asked May 24 '10 07:05

Priit


People also ask

How do you update parent and child records in JPA?

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.

How does hibernate versioning work?

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.

Why we use hibernate versioning?

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.


2 Answers

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
like image 137
Vlad Mihalcea Avatar answered Oct 12 '22 12:10

Vlad Mihalcea


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.

like image 37
Priit Avatar answered Oct 12 '22 12:10

Priit