Настройка Hibernate Envers

от автора

Поставили мне как-то задачу сделать аудирование в нашем сервисе. Немного почитав решил использовать Hibernate Envers, вроде всё должно работать из коробки и без проблем.

Хочу рассказать как этот «ВЖУХ» работает.

Вот небольшой тестовый проект, пара сущностей, контроллеры и стандартный CRUD. Нам интересны сущности, именно над ними нужно повешать аннотации.

Подготовка

@Data @Entity @Table(name = "message", schema = "forum") public class Message {      @Id     @SequenceGenerator(name = "message_generator", sequenceName = "message_seq", schema = "forum", allocationSize = 1)     @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "message_generator")     private Long id;      private String author;      private String msg;      @ManyToOne(optional = false, fetch = FetchType.LAZY)     @JoinColumn(name = "forum_id")     private Forum forum;  }
@Data @Entity @Table(name = "forum", schema = "forum") public class Forum {      @Id     @SequenceGenerator(name = "forum_generator", sequenceName = "forum_seq", schema = "forum", allocationSize = 1)     @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "forum_generator")     private Long id;     private String name;     private String description;      @OneToMany(mappedBy = "forum", fetch = FetchType.LAZY)     private List<Message> messages; }

Подключение

Теперь мы решаем добавить аудирование любых изменений в этих таблицах которые были произведены из кода. Для этого нам нужно добавить зависимость.

Gradle :    compile 'org.hibernate:hibernate-envers'  Maven:  <dependency>     <groupId>org.hibernate</groupId>     <artifactId>hibernate-envers</artifactId>     <version>${hibernate.version}</version> </dependency>

Далее добавляем аннотацию над нашими сущностями:

@Audited

Вот как теперь выглядят наши сущности:

@Data @Entity @Table(name = "message", schema = "forum") @Audited public class Message {      @Id     @SequenceGenerator(name = "message_generator", sequenceName = "message_seq", schema = "forum", allocationSize = 1)     @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "message_generator")     private Long id;      private String author;      private String msg;      @ManyToOne(optional = false, fetch = FetchType.LAZY)     @JoinColumn(name = "forum_id")     private Forum forum;  }

Вот такие таблицы создались. forum_aud, message_aud и revinfo. В таблице revinfo хранятся порядковый номер и время изменения, а в таблицах forum_aud и message_aud сами изменения и ссылка на запись в оригинальной таблице. Начнём со структуры таблиц: id— идентификатор записи в forum rev — идентификатор записи в revinfo, revtype— тип события 0(inser)-1(update)-2(delete) Остальные поля повторяют поля в основной таблице.

Проблемы

1. Первая неприятность которая встречается, не очень нравиться что все таблице в одной схеме, если таблиц будет 10 и ещё 10 для аудирования, будет хаос.

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

3. Если сейчас мы попробуем вставить запись, то упадём вот с такой ошибкой ERROR: relation «hibernate_sequence» does not exist Это из-за того что по дефолту идентификаторы в таблицу revinfo будут браться из hibernate_sequence, но её нет.

Поиск решений

  1. Для решения первой проблемы существует аннотация, вешается над классом

@AuditTable(value = "user_AUD", schema = "history")

Здесь мы можем указать схему и название таблицы, не забудьте заранее создать схему.

  1. С этим чуть посложнее, тут уже придётся немного поколдовать. Лёгкий способ это расширить наши основные таблицы, а если у нас 10 таблиц, а если это может что-то сломать, слишком много если, нам это не подходит. Тогда появляется такой функционал, мы можем вручную переопределить таблицу revinfo. Это мы можем сделать двумя путями

    1) Создать новую сущность и унаследовать её от DefaultRevisionEntity. После этого мы сможем добавлять любые поля.
    А также нужно создать слушателя и в нём имплементировать RevisionListener и переопределяем метод newRevision.

@Data @Entity @RevisionEntity(ExampleListener.class) @Table(name = "REVINFO", schema = "history") public class ExampleRevEntity extends DefaultRevisionEntity {      private String username;  }
public class ExampleListener implements RevisionListener {      @Override     public void newRevision(Object revisionEntity) {         ExampleRevEntity exampleRevEntity = (ExampleRevEntity) revisionEntity;          exampleRevEntity.setUsername("UserName");     } }

Теперь мы можем добавлять любые новые поля в ExampleRevEntity и описывать логику в ExampleListener в методе newRevision .

2) По сути тоже что и первый метод, только мы не наследуется от DefaultRevisionEntity , а сами создаем её и определяем все поля. В таком случае мы можем более гибко указывать всё что нам нужно, например как заполнять идентификатор, не из hibernate_sequence, а из своей sequence. Благодаря этому решаем проблему в третьем пункте.

@Data @Entity @RevisionEntity(ExampleListener.class) @Table(name = "REVINFO", schema = "history") public class ExampleRevEntity {      @Id     @RevisionNumber     @GeneratedValue(generator = "CustomerAuditRevisionSeq")     @SequenceGenerator(name = "CustomerAuditRevisionSeq", sequenceName = "customer_audit_revision_seq", schema = "history", allocationSize = 1)     private int id;      @RevisionTimestamp     private long timestamp;      private String username;  }

А вот теперь «ВЖУХ» и всё работает. Мы видим записи аудирования в нашей таблице.

Ещё проблемы

Ещё несколько проблем с которыми я столкнулся, но не описал выше.

  • Связи OneToMany и ManyToOne могут привезти к ошибке если обновление происходит сразу по нескольким сущностям

  • Если ваша сущность наследуется от другой, нужно аудировать и её поля

  • Проблема не существующих записей если у вас выбрана стратегия org.hibernate.envers.strategy.internal.ValidityAuditStrategy

Решения

  • Что-бы связи не ломали ваш процесс аудирования во-первых нужно аудировать и эти таблицы проделать пункты выше, второе эти поля нужно пометить аннотацией @AuditJoinTable Пример:

    @OneToMany(mappedBy = "forum", fetch = FetchType.LAZY)
    @AuditJoinTable private List<Message> messages;

  • Если вы унаследовали сущность от другой для аудирования вам нужно повешать над классом @AuditOverride Пример:
    @AuditOverride(forClass = ParentEntity.class)
    public class Forum extends ParentEntity

  • Мы можно менять стратегию аудирования, на ValidityAuditStrategy, при такой стратегии в таблице …_aud вы будете создавать ещё поле revend, это идентификатор записи которая перезатёрла эти изменения, так можно отслеживать актуальные записи.

    Но если у вас в таблицах уже есть данные, то при их изменении появиться новая запись об изменении и будет искаться старая запись, чтобы проставить ей revend, но так как такой записи нет, всё будет падать с ошибкой. К сожалению решения для этой проблемы я не нашёл, только накатывать данные после включения аудирования, либо не изменять старые данные.

Заключение

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

Источники

Официальная документация

https://vladmihalcea.com/the-best-way-to-implement-an-audit-log-using-hibernate-envers/

Ссылка на GitHub


ссылка на оригинал статьи https://habr.com/ru/post/715918/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *