【问题标题】:Hibernate versioning parent entityHibernate 版本控制父实体
【发布时间】:2011-02-23 03:20:54
【问题描述】:

考虑两个实体 Parent 和 Child。

  • 子项是父项临时集合的一部分
  • 子级使用 FetchType.LAZY 映射到父级的 ManyToOne

两者都在同一个表单上显示给用户。当用户保存数据时,我们首先更新父实例,然后更新子集合(都使用合并)。

现在是棘手的部分。当用户仅修改表单上的子属性时,休眠脏检查不会更新父实例,因此不会增加该实体的乐观锁定版本号。

我想看看只有 Parent 被版本化的情况,每次我为 Parent 调用 merge 然后版本总是更新,即使在 db 中没有执行实际更新。

【问题讨论】:

  • 您可能想在这种情况下回答您自己的问题。有人可能会来投票给你。或者更好的是,这可能会帮助正在寻找类似东西的人。

标签: java hibernate jpa transactions locking


【解决方案1】:

您可以将更改从子实体传播到父实体。这要求您在修改子实体时传播OPTIMISTIC_FORCE_INCREMENT 锁。

因此,您需要让所有实体都实现RootAware 接口:

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();
    }
}

那么,你需要两个事件监听器:

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);
    }
}

和

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

您可以按如下方式注册:

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

然后通过 Hibernate 配置属性提供 RootAwareFlushEntityEventListenerIntegrator:

configuration.put(
    "hibernate.integrator_provider", 
    (IntegratorProvider) () -> Collections.singletonList(
        RootAwareEventListenerIntegrator.INSTANCE
    )
);

现在,当您修改 PostCommentDetails 实体时:

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

父Post实体版本也被修改:

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

【讨论】:

  • 如何在 spring configuration.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList( RootAwareEventListenerIntegrator.INSTANCE) ) 中实现;
  • hibernate.integrator_provider 属性也可以采用完全限定的类名,因此您也可以通过 Spring 提供它。只需将逻辑(目前表示为 lambda)封装在专用类中即可。
  • 由于这个解决方案在技术上是正确的并且产生了正确的数据库状态,所以在休眠中仍然存在一个错误。 (hibernate.atlassian.net/browse/HHH-11810)。乐观锁增量在事务提交时验证。因此,如果您想将正确的版本返回给事务之外的调用者,它基本上是没有用的。我认为这是常见的情况,例如网络服务。
  • 该问题与手动调用刷新有关,无论如何这都是代码异味。 Flush 无论如何都会在提交之前调用,呈现预期的结果。
  • 但是 afaik Session.flush 和 Transaction.commit 之间是有区别的。仅使用刷新语句将在数据库级别执行,但提交将触发数据库端的提交。因此,如果您在刷新后执行某些代码并触发异常,则刷新的命令仍将被回滚。当事务已经提交时,情况并非如此。 @VladMihalcea:您有任何想法或示例如何从方法中返回正确的版本号并且对所有代码行都具有回滚意识?
【解决方案2】:

我想我明白了。调用合并后,将返回附加的实例引用。当我使用 entityManager.lock(updated, LockModeType.WRITE); 获得显式锁定时然后即使父实例未在 db 中更新,版本号也会增加。

此外,我将分离实例版本与持久实例版本进行比较。如果它们不匹配,则 Parent 已在 db 中更新,并且版本号也已更改。这使版本号保持一致。否则 entityManager.lock 会增加版本号,即使合并操作改变了它。

仍在寻找解决方案如何在合并期间实体不脏时使休眠增加版本。

【讨论】:

    【解决方案3】:

    我认为您不能强制休眠为未更改的对象增加版本号,因为如果没有任何更改(出于显而易见的原因),它将不会执行任何 db UPDATE 查询。

    您可以做一些令人讨厌的 hack,例如向对象添加一个新字段并手动递增该字段,但就个人而言,这似乎是在浪费时间和资源。我会选择您的显式锁定解决方案,因为这似乎可以满足您的需求,而无需不必要的黑客攻击。

    【讨论】:

      【解决方案4】:

      我刚刚实现了类似的东西,它像野兽一样又快又好。 目前您正在保存您的“孩子”,只需调用执行以下操作:

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

      您需要访问可以在 spring 中获得的实体管理器,例如:

        @PersistenceContext
        private EntityManager entityManager;
      

      你的父实体应该有来自 javax.persistence.Version 的 @Version 而不是 spring 的。 (我假设在孩子保存时刻,您将完成所有验证和操作,因此当您保存孩子时,父母肯定会变脏)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-03-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多