Hibernate版本控制父实体

5
考虑两个实体Parent和Child。
  • Child是Parent的瞬态集合的一部分
  • Child具有对Parent的ManyToOne映射,FetchType为LAZY
这两个实体都在同一个表单上向用户展示。当用户保存数据时,我们首先更新Parent实例,然后更新Child集合(都使用merge)。
现在来到棘手的部分。当用户仅修改表单上的Child属性时,Hibernate脏检查不会更新Parent实例,因此不会为该实体增加乐观锁定版本号。
我希望看到的情况是只有Parent被版本化,每次调用merge Parent时,即使实际更新未在数据库中执行,版本也始终会更新。

在这种情况下,您可能想回答自己的问题。有人可能会来给你点赞。或者更好的是,那可能会帮助正在寻找类似事物的人。 - Adeel Ansari
4个回答

6
你可以将子实体的更改传播到父实体。这需要你在修改子实体时传播 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配置中实现这一点。put( "hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList( RootAwareEventListenerIntegrator.INSTANCE ) ); - saferJo
hibernate.integrator_provider 属性也可以接受一个完全限定的类名,因此您也可以通过 Spring 提供它。只需将逻辑(当前表示为 lambda)封装在专用类中即可。 - Vlad Mihalcea
由于此解决方案在技术上是正确的并且产生了正确的数据库状态,但是Hibernate中仍存在一个错误(https://hibernate.atlassian.net/browse/HHH-11810)。乐观锁增量在事务提交时进行验证。因此,如果您想将正确的版本返回给事务之外的调用者,则基本上是无用的。我认为这是例如Web服务的常见情况。 - Auskennfuchs
这个问题与手动调用flush有关,这本身就是一种代码异味。无论如何,在提交之前都会调用Flush,从而产生预期的结果。 - Vlad Mihalcea
我不明白在你的情况下哪个方法会抛出异常。请在我的高性能Java持久化GitHub存储库中发送一个可复制的测试用例,之后我会仔细查看。 - Vlad Mihalcea
显示剩余3条评论

3

我想我弄清楚了。在调用合并操作后,将返回一个已附加的实例引用。当我使用entityManager.lock(updated, LockModeType.WRITE);为其获取显式锁时,即使父实例在数据库中未更新,版本号也会增加。

此外,我正在比较脱离的实例版本与持久化实例版本。如果它们不匹配,则父实例已在数据库中更新,并且版本号已更改。这保持了版本号的一致性。否则,entityManager.lock将增加版本号,即使合并操作对其进行了更改。

仍在寻找如何在合并期间实体未发生更改时使Hibernate增加版本的解决方案。


0

我刚刚实现了类似的东西,它的速度和效果都非常优秀。 目前只需要像这样调用即可保存你的“子项”:

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

你需要访问实体管理器,可以在Spring中获取:

  @PersistenceContext
  private EntityManager entityManager;

你的父实体应该使用 javax.persistence.Version 中的 @Version,而不是 Spring 的版本。(我假设在子保存时,您将完成所有验证和其他操作,因此当您保存子级时,父级肯定会变脏)


0

我认为你无法强制Hibernate为未更改的对象增加版本号,因为如果没有任何更改(出于明显的原因),它将不执行任何数据库UPDATE查询。

你可以像添加一个新字段并手动递增那样进行肮脏的黑客攻击,但个人认为这是浪费时间和资源。我会选择显式锁定解决方案,因为它似乎可以给你想要的东西,而不需要不必要的黑客行为。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接