ENVERS: автоматический аудит изменений в Hibernate

new_audit«…и мы должны иметь возможность видеть, когда, как и кто изменил данные» — распространённая хотелка при разработке приложения, работающего с БД.

Заказчик обычно хочет видеть, для некоторых таблиц, какие изменения в них вносились, когда вносились, кем вносились и какие были предыдущие значения. Существует множество решений этой задачи: можно обвесить необходимые таблицы триггерами, можно сохранять объекты с помощью хранимых процедур, можно перехватывать запросы к базе с помощью AOP и так далее. предлагает собственно решение, envers, решающее задачу ведения истории с помощью одной аннотации.

Настройка envers

Чтобы включить автоматическое ведение истории изменений, необходимо выполнить два шага:

  • Добавить библиотеку envers в classpath:
  • Добавить аннотацию @Audited к классам, для которых требуется отслеживание истории изменении. Рекоменуется добавить эту аннотацию так же и к классам, имеющим связи с классами, уже помеченными @Audited. Далее в статье я  буду использовать сущности из примера HQL.
Hibernate автоматически (если конечно включено hbm2ddl.auto) создаст следующие таблицы:

  • имя_сущности_AUD — для хранения исторических данных. Эти таблицы состоят из поля id отслеживамой сущности, поля rev, в котором хранится номер редакции, поля revtype, в котором хранится тип изменения (создано/модифицировано/удалено) и все отслеживаемые поля оригинальной сущности.
  • REVINFO — для хранения списка ревизий. В этой таблице хранятся номера ревизий и время их создания. Так же в этой таблице можно хранить дополнительные данные, такие как имя пользователя, создавшего ревизию, например.

Когда

Для начала ответим на вопрос «Когда были изменены данные». Создадим пробную персону и проверим, как это отразилось в истории.

Длинный код создания персоны

[свернуть]
Запросы к истории делаются с помощью AuditQuery, который можно запросить у AuditReaderFactory.

Запрос forRevisionsOfEntity() возвращает данные о изменениях сущности. Первый параметр задаёт класс сущности, второй параметр регулирует, хотим ли мы получить только ревизии ( true) или ревизии и данные сущностей ( false). В первом случае возвращаются объекты ревизий, во втором — массив из трех объектов: данные сущности в ревизии, ревизия, тип изменения. Наконец последний параметр регулирует, включать ли в вывод удалённые записи или нет.

В результате мы видим, что в ревизии 1, созданной сегодня, 25 августа, в 11:02, была добавлена сущность Person и отображены её данные.

Как

Когда изменились данные мы видим, но интересно было бы посмотреть в динамике — какие были значения, какие стали. Попробуем изменить ранее созданную персону:

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

Очевидно, что выборка по всей истории изменений не так интересна, как выборка только по интересующему объекту. В AuditQuery можно задавать критерии выборки используя похожий на Criteria API подход. Создадим новую персону и проведём над ней несколько действий:

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

Можно так же убедиться, что изменения в связанных таблицах так же записываются:

Наконец, кроме просмотра истории ревизий, можно запросить и конкретную ревизию:

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

Кто

Самый интересный вопрос — кто же внёс изменения в таблицу. Hibernate сам не записывает такую информацию, так как не знаком со структурой кажжого приложения в мире, поэтому для записывания данных пользователя придётся немного потрудится.

В первую очередь расширим сущность ревизии с тем, чтобы добавить в неё дополнительное поле:

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

Наконец, надо не забыть добавить собственную сущность ревизии в конфигурацию маппинга:

Результат на лицо:

Envers и JPA

Механизм envers будет работать и при использовании Hibernate в качестве JPA Implementation, но с небольшими изменениями. При создании AuditQuery надо передавать объект EntityManager, а не Session и не требуется явно регистрировать собственную сущность ревизий.

Код примера доступен на github.