Как реплицировать инструкции INSERT / UPDATE / DELETE с помощью JPA и Hibernate

Я хотел бы переименовать таблицу PostgreSQL (9.6) таким образом, чтобы ее можно было восстановить для моей системы (приложение java с использованием JPA / Hibernate)

В моем коде Java объект JPA будет иметь следующие аннотации @Entity@Table(name = "old_name"), а в базе данных будет эквивалентная таблица с именем old_name.

Я хотел бы переименовать таблицу в new_name таким образом, чтобы я мог постепенно обновлять базу данных и приложение Java, допуская сбой и откат.

Типичные шаги были бы

  1. создать копию old_name в new_name
  2. убедитесь, что чтение / запись доступны в обоих режимах (т. е. данные реплицируются в обоих направлениях)
  3. обновить приложение Java, чтобы использовать новую таблицу new_name
  4. когда завершится уверенное обновление системы, удалите old_name

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

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

Я попытался переименовать таблицу и создать над ней «простое представление», однако объект JPA жалуется, что не может найти таблицу с именем представления. (Потому что это представление, а не таблица :) и, похоже, нет аннотации @ View / @ Table JPA, которая справится с этим)

Я еще не пробовал средства, перечисленные здесь: http://wiki.postgresql.org/wiki/Replication,_Clustering,_and_Connection_Pooling, поскольку большинство, похоже, связано с объединением, сегментированием, и мне нужна простая краткосрочная реплика таблицы, но я буду исследовать и их.

Спасибо - я, конечно, хотел бы самый простой вариант, предпочтя что-то встроенное в postgres / JPA, но также серьезно рассмотрю сторонние варианты.

Удовольствие от слоев обфускации, представление было бы самым простым и эффективным решением. Ваш уровень запутывания "принимает" стороннюю таблицу?

a_horse_with_no_name 12.12.2018 07:47

Честно говоря, я не знаю, хотя я ожидал, что это будет более вероятно, поскольку в postgres он рассматривается как таблица, а не представление. По крайней мере, Postgres делает вид, что это таблица :) @a_horse_with_no_name

Mitch Kent 12.12.2018 10:13

Другой вариант - логическая репликация

a_horse_with_no_name 12.12.2018 10:15

логическая репликация была бы сложной, потому что соединения между реплицированными таблицами должны быть выполнены в «режиме репликации», и поэтому процесс автоматического выпуска должен будет управлять этим (или, что еще хуже, мне придется). Выполнение этого в коде, таком как прослушиватели сущностей ниже, означает, что он будет просто работать (tm) на всех службах и машинах разработчика.

Mitch Kent 12.12.2018 10:23
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
6
4
3 044
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Таблицы базы данных

Предположим, у вас есть две следующие таблицы:

CREATE TABLE old_post (
    id int8 NOT NULL,
    title varchar(255),
    version int4 NOT NULL,
    PRIMARY KEY (id)
)
                                                  
CREATE TABLE post (
    id int8 NOT NULL,
    created_on date, 
    title varchar(255),
    version int4 NOT NULL,
    PRIMARY KEY (id)
)

Объект JPA

Таблица old_post должна быть реплицирована с новым post. Обратите внимание, что таблица post теперь имеет больше столбцов, чем старая таблица.

Нам нужно только сопоставить сущность Post:

@Entity(name = "Post")
@Table(name = "post")
public static class Post {

    @Id
    private Long id;

    private String title;

    @Column(name = "created_on")
    private LocalDate createdOn = LocalDate.now();

    @Version
    private int version;

    //Getters and setters omitted for brevity
}

Слушатели событий гибернации

Теперь нам нужно зарегистрировать 3 прослушивателя событий для перехвата операций INSERT, UPDATE и DELETE для объекта Post.

Мы можем сделать это с помощью следующих прослушивателей событий:

public class ReplicationInsertEventListener 
        implements PostInsertEventListener {
 
    public static final ReplicationInsertEventListener INSTANCE = 
        new ReplicationInsertEventListener();
 
    @Override
    public void onPostInsert(
            PostInsertEvent event) 
            throws HibernateException {
        final Object entity = event.getEntity();
 
        if (entity instanceof Post) {
            Post post = (Post) entity;
 
            event.getSession().createNativeQuery(
                "INSERT INTO old_post (id, title, version) " +
                "VALUES (:id, :title, :version)")
            .setParameter("id", post.getId())
            .setParameter("title", post.getTitle())
            .setParameter("version", post.getVersion())
            .setFlushMode(FlushMode.MANUAL)
            .executeUpdate();
        }
    }
 
    @Override
    public boolean requiresPostCommitHanding(
            EntityPersister persister) {
        return false;
    }
}

public class ReplicationUpdateEventListener 
    implements PostUpdateEventListener {

    public static final ReplicationUpdateEventListener INSTANCE = 
        new ReplicationUpdateEventListener();

    @Override
    public void onPostUpdate(
            PostUpdateEvent event) {
        final Object entity = event.getEntity();

        if (entity instanceof Post) {
            Post post = (Post) entity;

            event.getSession().createNativeQuery(
                "UPDATE old_post " +
                "SET title = :title, version = :version " +
                "WHERE id = :id")
            .setParameter("id", post.getId())
            .setParameter("title", post.getTitle())
            .setParameter("version", post.getVersion())
            .setFlushMode(FlushMode.MANUAL)
            .executeUpdate();
        }
    }

    @Override
    public boolean requiresPostCommitHanding(
            EntityPersister persister) {
        return false;
    }
}

public class ReplicationDeleteEventListener 
        implements PreDeleteEventListener {

    public static final ReplicationDeleteEventListener INSTANCE = 
        new ReplicationDeleteEventListener();

    @Override
    public boolean onPreDelete(
            PreDeleteEvent event) {
        final Object entity = event.getEntity();

        if (entity instanceof Post) {
            Post post = (Post) entity;

            event.getSession().createNativeQuery(
                "DELETE FROM old_post " +
                "WHERE id = :id")
            .setParameter("id", post.getId())
            .setFlushMode(FlushMode.MANUAL)
            .executeUpdate();
        }

        return false;
    }
}

С помощью Hibernate Integrator можно зарегистрировать 3 слушателя событий:

public class ReplicationEventListenerIntegrator 
        implements Integrator {

    public static final ReplicationEventListenerIntegrator INSTANCE = 
        new ReplicationEventListenerIntegrator();

    @Override
    public void integrate(
            Metadata metadata,
            SessionFactoryImplementor sessionFactory,
            SessionFactoryServiceRegistry serviceRegistry) {

        final EventListenerRegistry eventListenerRegistry =
                serviceRegistry.getService(EventListenerRegistry.class);

        eventListenerRegistry.appendListeners(
            EventType.POST_INSERT, 
            ReplicationInsertEventListener.INSTANCE
        );

        eventListenerRegistry.appendListeners(
            EventType.POST_UPDATE, 
            ReplicationUpdateEventListener.INSTANCE
        );

        eventListenerRegistry.appendListeners(
            EventType.PRE_DELETE, 
            ReplicationDeleteEventListener.INSTANCE
        );
    }

    @Override
    public void disintegrate(
            SessionFactoryImplementor sessionFactory,
            SessionFactoryServiceRegistry serviceRegistry) {

    }
}

И, чтобы указать Hibernate использовать этот пользовательский Integrator, вам необходимо настроить свойство конфигурации hibernate.integrator_provider:

<property name = "hibernate.integrator_provider"
          value = "com.vladmihalcea.book.hpjp.hibernate.listener.ReplicationEventListenerIntegrator "/>

Время тестирования

Теперь при сохранении сущности Post:

Post post1 = new Post();
post1.setId(1L);
post1.setTitle(
    "The High-Performance Java Persistence book is to be released!"
);

entityManager.persist(post1);

Hibernate выполнит следующие инструкции SQL INSERT:

Query:["INSERT INTO old_post (id, title, version) VALUES (?, ?, ?)"], Params:[(1, The High-Performance Java Persistence book is to be released!, 0)]

Query:["insert into post (created_on, title, version, id) values (?, ?, ?, ?)"], Params:[(2018-12-12, The High-Performance Java Persistence book is to be released!, 0, 1)]

При выполнении другой транзакции, которая обновляет существующую сущность Post и создает новую сущность Post:

Post post1 = entityManager.find(Post.class, 1L);
post1.setTitle(post1.getTitle().replace("to be ", ""));

Post post2 = new Post();
post2.setId(2L);
post2.setTitle(
    "The High-Performance Java Persistence book is awesome!"
);

entityManager.persist(post2);

Hibernate также копирует все действия в таблицу old_post:

 Query:["select tablerepli0_.id as id1_1_0_, tablerepli0_.created_on as created_2_1_0_, tablerepli0_.title as title3_1_0_, tablerepli0_.version as version4_1_0_ from post tablerepli0_ where tablerepli0_.id=?"], Params:[(1)]

 Query:["INSERT INTO old_post (id, title, version) VALUES (?, ?, ?)"], Params:[(2, The High-Performance Java Persistence book is awesome!, 0)]

 Query:["insert into post (created_on, title, version, id) values (?, ?, ?, ?)"], Params:[(2018-12-12, The High-Performance Java Persistence book is awesome!, 0, 2)]

 Query:["update post set created_on=?, title=?, version=? where id=? and version=?"], Params:[(2018-12-12, The High-Performance Java Persistence book is released!, 1, 1, 0)]

 Query:["UPDATE old_post SET title = ?, version = ? WHERE id = ?"], Params:[(The High-Performance Java Persistence book is released!, 1, 1)]

При удалении объекта Post:

Post post1 = entityManager.getReference(Post.class, 1L);
entityManager.remove(post1);

Также удаляется запись old_post:

Query:["DELETE FROM old_post WHERE id = ?"], Params:[(1)]
Query:["delete from post where id=? and version=?"], Params:[(1, 1)]

Код доступен на GitHub.

Это отличный ответ - спасибо. Я предполагаю, что этот вторичный dml будет зафиксирован в той же транзакции?

Mitch Kent 12.12.2018 17:26

Важное замечание для людей, которые придут к этому позже - для меня важна строка `.setFlushMode (FlushMode.MANUAL)`, так как без нее вы получите попытки «двойного сброса». Я копировал этот подход по памяти, а не дословно, и потратил слишком много времени, пытаясь выяснить, что пошло не так :)

Mitch Kent 18.01.2019 12:20

Другие вопросы по теме