简体   繁体   中英

Trouble with hibernate optimistic lock and collections

I have a trouble in using optimistic locking. I have a persistent object, with a version number. I want this version number to be increased only when my persistent object is 'really' updated, meaning when one or more fields have been modified or when a collection mapped in my entity with a @ManyToOne or @OneToMany annotation is modified in the database. What happens is that the version is increased only when a field contained directly in my entity has changed, and not when a collection has changed .

Note : I put select-before-update in my Entity annotation. Don't know if it might changed the behaviour of the versioning on the collections! I also have a field which should not influence the version in my entity, on which I put the @OptimisticLock(exclude=true) annotation.

Does anyone know how I may try to make my versioning work? According to what I've read on several forums, the version number should be automatically increased when a collection changed. Why wouldn't it be the case in my case? Any idea?

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 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

The same goes for the PostComment entity as well.

And it works even if you insert a new child entity:

Post post = entityManager.getReference(Post.class, 1L);

PostComment postComment = new PostComment();
postComment.setId(3L);
postComment.setReview("Worth it!");
postComment.setPost(post);
entityManager.persist(postComment);

Hibernate managing to increment the parent entity properly:

SELECT p.id AS id1_0_0_ ,
       p.title AS title2_0_0_ ,
       p.version AS version3_0_0_
FROM   post p
WHERE  p.id = 1

INSERT INTO post_comment (post_id, review, id) 
VALUES (1, 'Worth it!', 3)

UPDATE post 
SET version = 3 
WHERE id = 1 AND version = 2

It also works when removing child entities:

PostComment postComment = entityManager.getReference(PostComment.class, 3l);
entityManager.remove(postComment);

Hibernate managing to increment the parent entity in this use case as well:

SELECT pc.id AS id1_1_0_ ,
       pc.post_id AS post_id3_1_0_ ,
       pc.review AS review2_1_0_
FROM   post_comment pc
WHERE  pc.id = 3
 
SELECT p.id AS id1_0_0_ ,
       p.title AS title2_0_0_ ,
       p.version AS version3_0_0_
FROM   post p
WHERE  p.id = 1
 
DELETE FROM post_comment 
WHERE id = 3
 
UPDATE post 
SET version = 4 
WHERE id = 1 and version = 3

I thought, after reading documentation as this : https://docs.redhat.com/docs/en-US/JBoss_Enterprise_Web_Server/1.0/html/Hibernate_Annotations_Reference_Guide/ch03s04s03s08.html , that @OptimisticLock(excluded = true) prevent the version to be incremented. So in the example you give, if you put it on the field mapping the collection it would not act correctly.

I think the good exemple should be :

@OneToMany(mappedBy = "parent", cascade = {CascadeType.ALL})
public Set<Child> getChildren()
{
    return children;
}

or

@OneToMany(mappedBy = "parent", cascade = {CascadeType.ALL})
@OptimisticLock(excluded = false)
public Set<Child> getChildren()
{
    return children;
}

Select before update only retrieves the state of the entity instance and not the collections and associations. So when you update the collection, and then update the entity, the select before update ignores the collection update. You can try to use merge operation which loads the entity (including collections) and copies the changes from detached instance and hands you the entity which is persistent.

You should add some parameters (CascadeType) to @OneToMany annotation, eg (parent entity)

@OneToMany(mappedBy = "parent", cascade = {CascadeType.ALL})
@OptimisticLock(excluded = true)
public Set<Child> getChildren()
{
    return children;
}

And in @ManyToOne (child entity)

@ManyToOne
@OptimisticLock(excluded = true)

Example http://shrubbery.homeip.net/c/display/W/Hibernate+JPA+Tips (domain name changed)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM