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

Лучший способ реализовать журнал аудита с помощью Hibernate Envers

Узнайте, как лучше всего реализовать журнал аудита для отслеживания инструкций ВСТАВКИ, ОБНОВЛЕНИЯ и УДАЛЕНИЯ с помощью Hibernate Envers.

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

Вступление

В этой статье мы узнаем, как лучше всего реализовать журнал аудита для отслеживания инструкций ВСТАВКИ, ОБНОВЛЕНИЯ и УДАЛЕНИЯ с помощью Hibernate Envers .

Как объяснялось ранее , CDC (Сбор данных об изменениях) является важным шагом для извлечения событий изменений из приложения OLTP, чтобы сделать их доступными для других модулей в корпоративной системе (например, кэш, хранилище данных).

Хотя Debezium является наиболее эффективным способом выполнения CDC , возможно, вам понадобится более простое решение в вашем проекте. Hibernate Envers-это расширение Hibernate ORM, которое позволяет мгновенно фиксировать события изменений.

В этой статье мы рассмотрим наиболее эффективный способ использования энверов Hibernate.

Спящий режим Энверс-зависимость от Maven

Поскольку Hibernate Envers упакован как отдельная зависимость, если вы хотите ее использовать, вам необходимо объявить следующую зависимость Maven:


    org.hibernate
    hibernate-envers
    ${hibernate.version}

Спящий режим Envers @Проверенная аннотация

Теперь, после добавления зависимости hibernate-envers , вам необходимо указать Hibernate, какие объекты должны быть проверены, и это можно сделать с помощью аннотации @Audited на уровне сущности.

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

    @Id
    private Long id;

    private String title;

    //Getters and setters omitted for brevity

    @Override
    public String toString() {
        return "Post{" +
                "id=" + id +
                ", title='" + title + '\'' +
                '}';
    }
}

Стратегия ведения журнала аудита по умолчанию в режиме гибернации Envers

Как только вы добавите аннотацию @Audited к своей сущности и сгенерируете схему базы данных с помощью инструмента hbm2ddl , будут созданы следующие таблицы аудита:

CREATE TABLE post_AUD
(
    id      BIGINT NOT NULL,
    REV     INTEGER NOT NULL,
    REVTYPE TINYINT,
    title   VARCHAR(255),
    PRIMARY KEY ( id, REV )
)
    
CREATE TABLE revinfo 
( 
    rev      INTEGER GENERATED BY DEFAULT 
             AS IDENTITY ( START WITH 1 ), 
    revtstmp BIGINT, 
    PRIMARY KEY ( rev ) 
)   

ALTER TABLE post_aud 
ADD CONSTRAINT 
    fkllaf9w93qaiooguo8mfvwtwbg 
FOREIGN KEY ( REV ) REFERENCES revinfo 

Использование hbm2ddl не рекомендуется для производственных сред, поэтому используйте его только для создания прототипа схемы базы данных. Поэтому вам следует предпочесть использовать инструмент автоматической миграции схем, такой как Flyway .

Тем не менее, Hibernate ожидает найти вышеупомянутые таблицы аудита в вашей базе данных, в противном случае Envers потерпит неудачу. Поэтому убедитесь, что вы добавили их в свои сценарии миграции вместе с реальными таблицами сущностей.

В таблице revinfo хранится номер редакции и ее временная метка эпохи, в то время как в таблице post_AUD хранится снимок сущности в определенной редакции.

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

В столбце EVTYPE хранится порядковый номер Типа редакции Перечисления, который инкапсулирует одно из следующих изменений состояния сущности:

  • ДОБАВИТЬ – an ВСТАВИТЬ Инструкция SQL создала рассматриваемую сущность
  • MOD – инструкция UPDATE SQL изменила нашу сущность
  • Инструкция DEL – a DELETE SQL удалила проверяемый объект из таблицы базы данных

Сохранение сущности

При создании Записи сущности и сохранении в текущем контексте сохранения:

Post post = new Post();
post.setId( 1L );
post.setTitle( 
    "High-Performance Java Persistence 1st edition" 
);
entityManager.persist( post );

Hibernate генерирует следующие три инструкции SQL INSERT :

INSERT INTO post (title, id) 
VALUES ('High-Performance Java Persistence 1st edition', 1)
    
INSERT INTO REVINFO (REV, REVTSTMP) 
VALUES (default, 1503062974131)

INSERT INTO post_AUD (REVTYPE, title, id, REV) 
VALUES (0, 'High-Performance Java Persistence 1st edition', 1, 1)
  • Первый оператор создает сущность Post и выполняется с помощью Hibernate ORM.
  • Второй оператор выполняется Envers для создания новой редакции.
  • Третий оператор также выполняется Envers для фиксации состояния объекта Post в этой редакции.

Обратите внимание, что значение столбца REV TYPE равно 0, что соответствует типу редакции. ДОБАВЬТЕ значение перечисления.

Обновление сущности

При обновлении ранее созданной Записи сущности:

Post post = entityManager.find( Post.class, 1L );

post.setTitle( 
    "High-Performance Java Persistence 2nd edition" 
);

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

SELECT p.id as id1_0_0_, p.title as title2_0_0_ 
FROM   post p 
WHERE  p.id = 1

UPDATE post 
SET    title = 'High-Performance Java Persistence 2nd edition' 
WHERE  id = 1

INSERT INTO REVINFO (REV, REVTSTMP) 
VALUES (default, 1503064508185)

INSERT INTO post_AUD (REVTYPE, title, id, REV) 
VALUES (1, 'High-Performance Java Persistence 2nd edition', 1, 2)
  • Первые два оператора генерируются Hibernate ORM для загрузки и изменения сущности.
  • Вторые утверждения генерируются Envers для фиксации новой редакции и нового состояния Post сущности.

Обратите внимание, что значение столбца REV TYPE равно 1, что соответствует типу редакции. MOD значение перечисления.

Удаление сущности

При удалении Записи сущности:

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

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

DELETE FROM post 
WHERE id = 1

INSERT INTO REVINFO (REV, REVTSTMP) 
VALUES (default, 1503065294147)

INSERT INTO post_AUD (REVTYPE, title, id, REV) 
VALUES (2, NULL(VARCHAR), 1, 3)

После удаления сущности из таблицы базы данных с помощью Hibernate ORM Envers вставляет новую редакцию, и все свойства, за исключением идентификатора сущности, будут установлены в null .

Обратите внимание, что значение столбца ТИП REV равно 2, что соответствует типу Редакции. DEL значение перечисления.

Запрос моментальных снимков сущности

Hibernate Envers предлагает AuditReaderFactory , который использует JPA EntityManager или Hibernate Сессия экземпляр и генерирует объект AuditReader .

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

List posts = AuditReaderFactory.get( entityManager )
.createQuery()
.forRevisionsOfEntity( Post.class, true, true )
.add( AuditEntity.id().eq( 1L ) )
.getResultList();

assertEquals( 3, posts.size() );

for ( int i = 0; i < posts.size(); i++ ) {
    LOGGER.info( 
        "Revision {} of Post entity: {}", 
        i + 1, 
        posts.get( i ) 
    );
}

При запуске приведенного выше тестового случая Hibernate генерирует следующие выходные данные:

SELECT   p.id as id1_1_, p.REV as REV2_1_, 
         p.REVTYPE as REVTYPE3_1_, p.title as title4_1_ 
FROM     post_AUD p 
WHERE    p.id = 1 
ORDER BY p.REV ASC

-- Revision 1 of Post entity: Post{id=1, title='High-Performance Java Persistence 1st edition'}
-- Revision 2 of Post entity: Post{id=1, title='High-Performance Java Persistence 2nd edition'}
-- Revision 3 of Post entity: Post{id=1, title='null'}

Пока все идет так хорошо!

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

List revisions = doInJPA( entityManager -> {
    return AuditReaderFactory.get( entityManager ).getRevisions(
        Post.class, 1L
    );
} );

Если вы хотите загрузить Запись моментальный снимок сущности на момент данной ревизии:

Post post = (Post) AuditReaderFactory.get( entityManager )
.createQuery()
.forEntitiesAtRevision( Post.class, revisions.get( 0 ) )
.getSingleResult();

assertEquals(
    "High-Performance Java Persistence 1st edition", 
    post.getTitle()
);

Hibernate Envers собирается сгенерировать SQL-запрос, подобный этому:

SELECT p1.id AS id1_1_,
       p1.REV AS REV2_1_,
       p1.REVTYPE AS REVTYPE3_1_,
       p1.title AS title4_1_
FROM   post_AUD p1
WHERE   
( 
    p1.REV IN 
    ( 
        SELECT MAX(p2.REV)
        FROM   post_AUD p2
        WHERE  p2.REV <= 1
               AND p1.id = p2.id 
    ) 
)
AND p1.REVTYPE <> 2

Ну, это определенно не очень эффективно!

Запросы, подобные приведенному выше, генерируются DefaultAuditStrategy и для очень большого журнала аудита они работают не так хорошо.

К счастью, Envers предлагает стратегию ValidityAuditStrategy для решения этой проблемы.

Гибернация включает в себя стратегию проверки достоверности, которая приходит на помощь

Чтобы переключиться с DefaultAuditStrategy на ValidityAuditStrategy , у вас есть свойство предоставить следующую конфигурацию гибернации:


Теперь при создании схемы базы данных с помощью инструмента hbm2ddl вместо этого будут созданы следующие таблицы аудита:

CREATE TABLE post_AUD
(
    id BIGINT NOT NULL ,
    REV INTEGER NOT NULL ,
    REVTYPE TINYINT ,
    REVEND INTEGER ,
    title VARCHAR(255) ,
    PRIMARY KEY ( id, REV )
)

CREATE TABLE revinfo 
( 
    rev      INTEGER GENERATED BY DEFAULT 
             AS IDENTITY ( START WITH 1 ), 
    revtstmp BIGINT, 
    PRIMARY KEY ( rev ) 
)  
    
ALTER TABLE post_AUD ADD CONSTRAINT 
    FKllaf9w93qaiooguo8mfvwtwbg 
FOREIGN KEY ( REV ) REFERENCES revinfo

ALTER TABLE post_AUD ADD CONSTRAINT 
    FKmo46u9kx2pmomhkxbmctlbwmg 
FOREIGN KEY ( REVEND ) REFERENCES revinfo

Таблица revinfo идентична таблице DefaultAuditStrategy , но в post_AWD есть новый столбец ПЕРЕМОТКА НАЗАД , который ссылается на таблицу revinfo и отмечает последнюю редакцию, для которой этот снимок сущности все еще действителен.

Сохранение сущности

При сохранении одной и той же сущности Post Hibernate генерирует следующие инструкции SQL:

INSERT INTO post (title, id) 
VALUES ('High-Performance Java Persistence 1st edition', 1)
    
INSERT INTO REVINFO (REV, REVTSTMP) 
VALUES (default, 1503116813359)

INSERT INTO post_AUD (REVTYPE, REVEND, title, id, REV)
VALUES (0, NULL(INTEGER), 'High-Performance Java Persistence 1st edition', 1, 1)

Значение столбца REVEND равно NULL , так что таким образом, эта запись журнала аудита является действительной.

Обновление сущности

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

SELECT p.id as id1_0_0_, p.title as title2_0_0_ 
FROM   post p 
WHERE  p.id = 1

UPDATE post 
SET    title = 'High-Performance Java Persistence 2nd edition' 
WHERE  id = 1

INSERT INTO REVINFO (REV, REVTSTMP) 
VALUES (default, 1503117067335)

INSERT INTO post_AUD (REVTYPE, REVEND, title, id, REV)
VALUES (1, NULL(INTEGER), 'High-Performance Java Persistence 2nd edition', 1, 2)

UPDATE post_AUD 
SET    REVEND = 2 
WHERE  id = 1 
       AND REV <> 2 
       AND REVEND IS NULL

Мало того, что добавляется новая запись журнала аудита Post сущности, но и предыдущая запись обновляется, чтобы отметить, что она больше недействительна.

По сравнению с DefaultAuditStrategy , ValidityAuditStrategy выполняет больше работы при хранении журналов аудита, поскольку ему необходимо обновить конец ревизии.

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

Удаление сущности

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

DELETE FROM post 
WHERE id = 1

INSERT INTO REVINFO (REV, REVTSTMP) 
VALUES (default, 1503117987724)

INSERT INTO post_AUD (REVTYPE, REVEND, title, id, REV)
VALUES (2, NULL(INTEGER), NULL(VARCHAR), 1, 3)

UPDATE post_AUD 
SET    REVEND = 3 
WHERE  id = 1 
       AND REV <> 3 
       AND REVEND IS NULL

Как и в случае с обновлением Post сущности, удаление сущности также будет означать конец ревизии для ранее сохраненной записи журнала аудита.

Запрос моментальных снимков сущности

При извлечении всех Post снимков состояния сущности генерируется один и тот же SQL-запрос, например DefaultAuditStrategy While fetching all Post entity state snapshots generates the same SQL query like DefaultAuditStrategy

SELECT p.id as id1_1_, p.REV as REV2_1_, 
       p.REVTYPE as REVTYPE3_1_, p.REVEND as REVEND4_1_, 
       p.title as title5_1_ 
FROM   post_AUD p 
WHERE  p.REV <= 1 
       AND p.REVTYPE <> 2 
       AND ( p.REVEND > 1 OR p.REVEND IS NULL)

Намного лучше, чем использовать коррелированный подзапрос! Чтобы ускорить этот тип запроса, мы могли бы добавить индекс для столбцов REV и REV И , таким образом, избегая последовательного сканирования.

Вывод

Hibernate Envers чрезвычайно прост в настройке, что делает его очень привлекательным выбором для приложений на основе Hibernate. Хотя DefaultAuditStrategy выполняет меньше работы при выполнении транзакции изменения состояния сущности, ValidityAuditStrategy является гораздо лучшим выбором при попытке ускорить получение моментальных снимков сущности.

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