Рубрики
Без рубрики

Как увеличить версию родительской сущности всякий раз, когда дочерняя сущность изменяется с помощью JPA и гибернации

Автор оригинала: Vlad Mihalcea.

StackOverflow и форум Hibernate – это золотые прииски. Вчера я наткнулся на следующий вопрос на нашем форуме :

Обычно обоснование объединения объектов в кластеры заключается в формировании транзакционной границы, внутри которой защищены бизнес-инварианты. Я заметил, что при ОПТИМИСТИЧНОМ режиме блокировки изменения дочерней сущности не приведут к увеличению версии в корневом каталоге. Такое поведение делает совершенно бесполезным объединение объектов вместе в первую очередь.

Есть ли способ настроить режим гибернации таким образом, чтобы любые изменения в кластере объектов приводили к увеличению версии корневого объекта? Я читал об OPTIMISTIC_FORCE_INCREMENT но я думаю, что это увеличивает версию независимо от того, были ли изменены сущности или нет. Поскольку в большинстве сценариев читатели не должны конфликтовать с другими читателями, это тоже не кажется таким уж полезным.

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

Какой блестящий вопрос! Этот пост продемонстрирует, насколько легко вы можете реализовать такое требование при использовании Hibernate.

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

Сообщение является корневой сущностью, и у него может быть несколько Комментариев к сообщению сущностей. Каждый Комментарий к сообщению может содержать не более одного Подробного комментария к сообщению The Post

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

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

    @Id
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private PostComment comment;

    private int votes;

    //Getters and setters omitted for brevity

    @Override
    public Post root() {
        return comment.root();
    }
}

Как вы, вероятно, заметили, ассоциация @OneToOne использует потрясающее @Maps отображение, которое я уже объяснял в этот пост .

Комментарий к сообщению и Детали комментария к сообщению сущности реализуют Корневой интерфейс, который очень прост:

public interface RootAware {
    T root();
}

Реализуя интерфейс Root Aware , мы можем разрешить корневую сущность для любого Комментария к сообщению и Сведений о комментариях к сообщению сущности.

Вопреки распространенному мнению, Hibernate-это не просто платформа ORM, а очень настраиваемая платформа доступа к данным. В нашем примере нам нужно перехватить любую модификацию дочерней сущности и получить событие OPTIMISTIC_FORCE_INCREMENT для связанной корневой сущности.

Для перехвата событий ОБНОВЛЕНИЯ и УДАЛЕНИЯ SQL необходим следующий прослушиватель событий пользовательских сущностей:

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

Этот прослушиватель событий будет выполняться всякий раз, когда объект сбрасывается текущим контекстом сохранения This event listener is going to be executed whenever an entity is flushed by the currently running Persistence Context . Every entity modification is automatically detected by the dirty checking mechanism and marked as dirty

Если сущность загрязнена и реализует Корневой интерфейс, мы можем просто заблокировать родительскую сущность с помощью OPTIMISTIC_FORCE_INCREMENT типа блокировки. Этот тип блокировки приведет к увеличению версии корневой сущности во время операции очистки.

Для перехвата, когда сохраняются новые дочерние сущности, необходим следующий прослушиватель событий:

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

Чтобы зарегистрировать эти два прослушивателя событий, нам необходимо предоставить org.hibernate.integrator.spi. Интегратор внедрение:

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

При начальной загрузке JPA EntityManagerFactory мы можем предоставить интегратор с поддержкой корневого списка событий через свойство конфигурации hibernate.integrator_provider :

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

Чтобы узнать, как можно задать свойство конфигурации hibernate.integrator_provider при использовании Spring с JPA или Spring с Hibernate, ознакомьтесь с этой статьей .

Предполагая, что в нашей системе есть следующие сущности:

doInJPA(entityManager -> {
    Post post = new Post();
    post.setId(1L);
    post.setTitle("High-Performance Java Persistence");

    PostComment comment1 = new PostComment();
    comment1.setId(1L);
    comment1.setReview("Good");
    comment1.setPost(post);

    PostCommentDetails details1 = new PostCommentDetails();
    details1.setComment(comment1);
    details1.setVotes(10);

    PostComment comment2 = new PostComment();
    comment2.setId(2L);
    comment2.setReview("Excellent");
    comment2.setPost(post);

    PostCommentDetails details2 = new PostCommentDetails();
    details2.setComment(comment2);
    details2.setVotes(10);

    entityManager.persist(post);
    entityManager.persist(comment1);
    entityManager.persist(comment2);
    entityManager.persist(details1);
    entityManager.persist(details2);
});

Обновление дочерних сущностей

При обновлении Детали комментария к сообщению сущность:

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

Hibernate создает следующие инструкции SQL:

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

Как вы можете видеть, обновляется не только строка post_comment_details , но также увеличивается версия post .

То же самое касается Комментария к сообщению изменений сущности:

PostComment postComment = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "join fetch pc.post p " +
    "where pc.id = :id", PostComment.class)
.setParameter("id", 2L)
.getSingleResult();

postComment.setReview("Brilliant!");

Спящий режим, генерирующий следующие инструкции SQL:

SELECT  pc.id AS id1_1_0_ ,
        p.id AS id1_0_1_ ,
        pc.post_id AS post_id3_1_0_ ,
        pc.review AS review2_1_0_ ,
        p.title AS title2_0_1_ ,
        p.version AS version3_0_1_
FROM    post_comment pc
INNER JOIN post p ON pc.post_id = p.id
WHERE   pc.id = 2

UPDATE post_comment 
SET post_id = 1, review = 'Brilliant!' 
WHERE id = 2

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

Добавление новых дочерних сущностей

Родитель Версия Post сущности увеличивается, даже если сохраняется новая дочерняя сущность:

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 создает следующие инструкции SQL:

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

Удаление дочерних сущностей

Это решение работает даже при удалении существующих дочерних сущностей:

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

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

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

Круто, правда?

Синхронизация версии корневой сущности для каждой модификации дочерней сущности с помощью Hibernate довольно проста. Hibernate поддерживает множество механизмов управления параллелизмом, как показано в этом всеобъемлющем руководстве .

Код доступен на GitHub Code available on GitHub