Hibernate версия родительского объекта - PullRequest
4 голосов
/ 24 мая 2010

Рассмотрим две сущности: Родителя и Дитя.

  • Ребенок является частью временной коллекции Родителя
  • Ребенок имеет сопоставление ManyToOne с родителем с помощью FetchType.LAZY

Оба отображаются в одной форме для пользователя. Когда пользователь сохраняет данные, мы сначала обновляем родительский экземпляр, а затем дочернюю коллекцию (оба используют слияние).

Теперь самое сложное. Когда пользователь изменяет только свойство Child в форме, грязная проверка гибернации не обновляет экземпляр Parent и, следовательно, не увеличивает номер версии оптимистической блокировки для этого объекта.

Я хотел бы видеть ситуацию, когда версионным является только Parent, и каждый раз, когда я вызываю merge для Parent, версия всегда обновляется, даже если фактическое обновление не выполняется в БД.

Ответы [ 4 ]

3 голосов
/ 24 мая 2010

Думаю, я понял это. После вызова merge возвращается ссылка на прикрепленный экземпляр. Когда я получаю явную блокировку для этого с помощью entityManager.lock (обновлено, LockModeType.WRITE); тогда номер версии увеличивается, даже если родительский экземпляр не был обновлен в БД.

Кроме того, я сравниваю версию отдельного экземпляра с сохраненной версией экземпляра. Если они не совпадают, то родительский элемент был обновлен в БД, а также изменился номер версии. Это сохраняет номера версий согласованными. В противном случае entityManager.lock увеличит номер версии, даже если операция слияния изменит его.

Все еще ищем решение, как сделать версию в режиме гибернации, когда объект не загрязнен во время слияния.

2 голосов
/ 30 августа 2016

Вы можете распространять изменения от дочерних сущностей к родительским сущностям.Это требует, чтобы вы распространяли блокировку 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
    }
}

и затем предоставить RootAwareFlushEntityEventListenerIntegrator через свойство конфигурации Hibernate:

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
0 голосов
/ 25 апреля 2018

Я только что реализовал нечто подобное, которое работает как зверь быстро и красиво.В настоящее время вы сохраняете своего «ребенка», просто позвоните и сделайте что-то вроде:

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

Вам необходим доступ к менеджеру сущностей, который можно получить весной, например:

  @PersistenceContext
  private EntityManager entityManager;

Вашродительская сущность должна иметь @Version из javax.persistence.Version, а НЕ весеннюю.(Я предполагаю, что в момент сохранения ребенка у вас будут все проверки и все, что нужно, поэтому, когда вы спасаете ребенка, родитель должен обязательно испачкаться)

0 голосов
/ 24 мая 2010

Я не думаю, что вы можете заставить hibernate увеличить номер версии для неизмененного объекта, потому что он просто не будет выполнять любой запрос db UPDATE, если ничего не изменилось (по очевидным причинам).

Вы могли бы сделать неприятный хак, например, добавить новое поле к объекту и увеличить его вручную, но лично это кажется пустой тратой времени и ресурсов.Я бы пошел с вашим явным решением блокировки, так как это, кажется, дает вам то, что вы хотите, без лишних хакеров.

...