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

Комментарии (3)


  1. kacetal
    00.00.0000 00:00
    +1

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


    1. aleksandy
      00.00.0000 00:00
      +1

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


    1. miha5418 Автор
      00.00.0000 00:00
      +1

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