Поставили мне как-то задачу сделать аудирование в нашем сервисе. Немного почитав решил использовать 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/
Ссылка на GitHub