Hibernate versioning parent entity

前端 未结 4 1281
暖寄归人
暖寄归人 2021-01-13 17:51

Consider two entities Parent and Child.

  • Child is part of Parent\'s transient collection
  • Child has a ManyToOne mapping to parent with FetchType.LAZY
相关标签:
4条回答
  • 2021-01-13 18:30

    I just implemented something similar which is working like beast fast and nice. At the moment you are saving your 'child' just call do something like:

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

    You need access to the entity manager which can be obtained in spring like:

      @PersistenceContext
      private EntityManager entityManager;
    

    Your parent entity should have @Version from javax.persistence.Version and NOT the spring one. (I assume that on the child save moment you will have all the verifications and things done so when you are saving the child the parent should get dirty for sure)

    0 讨论(0)
  • 2021-01-13 18:33

    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
    
    0 讨论(0)
  • 2021-01-13 18:44

    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.

    0 讨论(0)
  • 2021-01-13 18:55

    i don't think you can force hibernate to increase a version number for a non-changed object, because it will just not do any db UPDATE query if nothing has changed (for obvious reasons).

    you could do a nasty hack like adding a new field to the object and incrementing that manually, but personally that seems to be a waste of time and resources. i'd go with your explicit locking solution since that seems to give you what you want, without unnecessary hackery.

    0 讨论(0)
提交回复
热议问题