Поставили мне как-то задачу сделать аудирование в нашем сервисе. Немного почитав решил использовать 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, но её нет.
Поиск решений
Для решения первой проблемы существует аннотация, вешается над классом
@AuditTable(value = "user_AUD", schema = "history")
Здесь мы можем указать схему и название таблицы, не забудьте заранее создать схему.
С этим чуть посложнее, тут уже придётся немного поколдовать. Лёгкий способ это расширить наши основные таблицы, а если у нас 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/