Аудит изменений в БД при использовании Hibernate

В ходе работы над приложением с использованием Spring, Spring Security, Spring Data и Hibernate возникла необходимость в проведении аудита всех изменений в базе. Требовалось сохранять информацию о том, кто, когда и какие изменения произвел. Ответы на последние два пункта можно было бы получить с помощью создания триггеров для всех таблиц, но триггерам неоткуда было бы получить информацию о пользователе, совершившим изменения, так как эта информация хранится в Spring'овом Security Context'е.

В связи с этим функцию аудита придется переложить на приложение, но как это сделать? Казалось бы, для этого могли бы подойти JPA Lifecycle Events. К сожалению, в них нет возможности сохранять новые сущности в БД, чего не хватит для реализации аудита — так, нельзя будет отследить удаление сущности.

К моему счастью, существует Hibernate Envers, основанный на перехватчиках и событиях Hibernate'а.

Начало работы

Начать работу с Envers в последних версиях Hibernate'а очень просто. Сперва нужно добавить соответствующую зависимость в ваше приложение (так, чтобы в итоге она оказалась в classpath'е запущенного приложения). Например, в maven при упаковке артефакта в war (или при использовании maven-assemply-plugin) достаточно сделать следующее:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-envers</artifactId>
    <version>5.3.6.Final</version>
</dependency>

Далее, необходимо пометить сущности (или конкретные поля), аудит которых вы хотите вести, аннотацией org.hibernate.envers.Audited. Если вы хотите вести аудит сущности за исключением некоторых полей, то вам может оказаться полезной аннотация NotAudited.

При выборе сущностей для аудита стоит обратить особое внимание на связи между ними. Если одна из сущностей в связи не помечена аннотацией @Audited, то это приведет к ошибке. Такие связи следует помечать аннотацией @Audited(targetAuditMode = NOT_AUDITED).

Для начала этого достаточно. Hibernate автоматически найдет Hibernate Envers jar, зарегистрирует все перехватчики и создаст несколько дополнительных таблиц: таблицы с историей для каждой сущности (и связей, организованных через промежуточные таблицы, вроде @ManyToMany или при использовании @JoinTable) с суффиксом aud, каждая из которых имеет дополнительную колонку с типом операции (0 - создание, 1 - обновление, 2 - удаление) и ссылку на таблицу revinfo — она содержит информацию по каждой транзакции - номер, время.

Дополнительная конфигурация

Многие аспекты работы Envers'а (например, суффикс таблиц аудита) поддаются конфигурации.

org.hibernate.envers.audit_table_prefixorg.hibernate.envers.audit_table_suffix — префикс и суффикс для автоматически созданных таблиц аудита; их название можно изменить с помощью аннотации @AuditTable.

org.hibernate.envers.global_with_modified_flag — следует ли хранить флаг модификации для каждого поля во всех сущностях (может быть переопределено аннотацией @Audited( withModifiedFlag=true ) ).

org.hibernate.envers.store_data_at_delete — сохранять ли данные при удалении сущности (в противном случае будет создана запись со всеми полями null).

org.hibernate.envers.track_entities_changed_in_revision — сохранять ли дополнительную информацию о том, какие сущности были изменены в ходе ревизии.

Подробнее можно посмотреть здесь.

Сохранение информации о пользователе

Таблицу revinfo можно менять на свое усмотрение. Это осуществляется следубщими средствами:

1. Аннотация @RevisionEntity. С ее помощью помечается сущность, которая заместит revinfo по умолчанию. Такая сущность обязана содержать поля @RevisionNumber и @RevisionTimestamp. Например, может получиться такая сущность:

@Entity
@Table(name = "revinfo")
@RevisionEntity(CustomRevisionListener.class)
public class CustomRevisionEntity {
    @RevisionNumber
    @Id
    @GeneratedValue
    private long id;

    @Temporal(TemporalType.TIMESTAMP)
    @RevisionTimestamp
    private Date timestamp;

    private String username;
    //getters, setters...
}

2. В аннотации @RevisionEntity указывается реализация интерфейса org.hibernate.envers.RevisionListener. Он содержит единственный метод void newRevision(Object revisionEntity), который будет вызываться для каждой новой сущности ревизии. Полученный объект можно будет менять по собственному усмотрению — например, получить из SecurityContext'а нужную информацию о пользователе (в нашем случае, его username).

package ru.edu.portfolio.domain;

import org.hibernate.envers.RevisionListener;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

public class CustomRevisionListener implements RevisionListener {
    @Override
    public void newRevision(Object revisionEntity) {
        CustomRevisionEntity entity = (CustomRevisionEntity) revisionEntity;
        SecurityContext ctx = SecurityContextHolder.getContext();

        Authentication auth = ctx.getAuthentication();
        if (auth != null && !auth.isAuthenticated())
            entity.setUserInfo(((String) auth.getPrincipal()));
    }
}

 

Засим откланиваюсь, прощайте.