Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hibernate @Version does not work with one-to-many

I have a hibernate entity with one-to-many association:

@Entity
public class Parent {
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    @Cascade(CascadeType.ALL)
    private Set<Child> children = new HashSet<Child>();

    @Version
    private Date version;
}

@Entity
public class Child {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;

    @Basic
    private String key;
}

*some annotations removed for clarity

Child entity maps on a table with composite primary key (KEY and PARENT_ID). The problem is when two users adds the same Child (with the same key) to the same Parent the cascade save (session.saveOrUpdate(parent)) fails with Child's primary key violation instead of optimistic lock failure.

If users change some other property in the Parent entity in addition to the collection, the optimistic lock works fine.

I could add some fictive property to the Parent class and change it every time when the collection changes and it will do the trick but it looks like a hack.

Or I could replace composite primary key to a surrogate one (by adding @Id).

The question is: What is the recommended approach of implementing optimistic locking in such a case?

Could be related to Hibernate @Version causing database foreign key constraint failure.

like image 824
ike3 Avatar asked Oct 02 '22 17:10

ike3


1 Answers

Only unidirectional collection changes are going to be propagated to the parent entity version. Because you are using a bidirectional association, it's the @ManyToOne side that will control this association, so adding/removing an entity in the parent-side collection is not going to affect the parent entity version.

However, you can still propagate changes from child entities to parent entities. This requires you to propagate the OPTIMISTIC_FORCE_INCREMENT lock whenever the child entity is modified.

In short, 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 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);
    }
}

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 85
Vlad Mihalcea Avatar answered Oct 05 '22 12:10

Vlad Mihalcea