Hibernate Envers — отслеживание версий (ревизий) класса сущности и извлечение ее хронологии
В Hibernate есть модуль, который позволяет отслеживать версии каждой сущности. В этой статье рассмотрим необходимые настройки и протестируем работу Hibernate Envers.
Используемые технологии
- Hibernate 5.0.1.Final
- Spring 4.1.5.RELEASE
- JPA 2.1
- Maven 3.2.5
- IntelliJ IDEA 14
- MySQL 5.6.25
Описание Hibernate Envers
Во многих приложениях необходимо сохранять версии записей (данных) для каждой операции вставки (записи), обновления или удаления. При этом должен быть способ получить определенную версию этой записи. Hibernate Envers (Entity Versioning System — система поддержки версий сущностей) — модуль Hibernate, который специально спроектирован для автоматизации поддержки версий сущностей.
Hibernate Envers поддерживает две различных стратегии аудита:
- По умолчанию — Hibernate Envers будет поддерживать столбец для номера версии записи. Каждый раз, когда запись вставляется или обновляется, в таблицу хронологии вставляется новая запись с номером версии, извлеченным из последовательности базы данных или таблицы.
- Аудит достоверности — при такой стратегии сохраняются начальная и конечная версии каждой записи хронологии. Каждый раз, когда запись вставляется или обновляется, в таблицу хронологии вставляется новая запись с номером начальной версии. В то же самое время предыдущая запись обновляется номером конечной версии. Также возможно сконфигурировать Hibernate Envers на запись метки времени, когда конечная версия было обновлена, в предыдущей записи хронологии.
1. Описание задачи
Необходимо настроить Hibernate Envers, создать необходимые таблицы и протестировать код, который продемонстрирует как происходит отслеживание версий сущностей.
2. Структура проекта
Проект основан сразу на нескольких статьях, т.к. здесь включены сразу несколько базовых технологий, таких как JPA, Hibernate. Если вам нужно увидеть только настройки, необходимые для Hibernate Envers, то можете переходить к следующему пункту. Если же вам нужны пошаговые инструкции по подключению базы данных, создания классов сущностей, а так же настройки JPA и Hibernate, то вам необходимо прочитать статьи из разделов по Spring Data JPA (раздел JPA) и раздел по Hibernate. Базовые статьи с настройками это: JPA – пример приложения Hello World и Hibernate – быстрый старт. Пример приложения Hello World.
Структура данного проекта:
В проект подключена таблица MySQL (подключение написано в указанных статьях), создан один интерфейс и его реализация. Имеются два автоматически созданных файла с настройками — spring-config.xml и persistence.xml (этот не используется).
3. Настройки pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ru.javastudy</groupId> <artifactId>hibernate_envers</artifactId> <version>1.0-SNAPSHOT</version> <properties> <hibernate-version>5.0.1.Final</hibernate-version> <spring-framework-version>4.1.5.RELEASE</spring-framework-version> <spring-data-version>Gosling-RELEASE</spring-data-version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-bom</artifactId> <version>${spring-framework-version}</version> <scope>import</scope> <type>pom</type> </dependency> </dependencies> </dependencyManagement> <dependencies> <!--driver for connection to MySQL database --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.34</version> </dependency> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> </dependency> <!-- Hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate-version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> <version>${hibernate-version}</version> </dependency> <!-- Joda-Time - API uses in Spring Data–>--> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.7</version> </dependency> <!-- DATE TIME FOR HIBERNATE 4.0+, for Hibernate <4.0 use joda-time-hibernate version 1.3 --> <dependency> <groupId>org.jadira.usertype</groupId> <artifactId>usertype.jodatime</artifactId> <version>2.0.1</version> </dependency> </dependencies> </project> |
Добавлен необходимый минимум для запуска — две зависимости для spring, две для Hibernate, а так же используемая фреймворком библиотека для JodaTime (они разные для версий до Hibernate 4.0 и после!).
4. Создание таблиц для Hibernate Envers
Базовой таблицей станет ContactAuditEntity (впервые добавлена в Spring Data JPA – отслеживание изменений в сущностном классе (Spring Data JPA Auditing). В неё будут записываться записи, версии которых мы и будем отслеживать.
Отмечу, что указанная чуть выше статья описывает отслеживание лишь изменений в записях (кто и когда создал\изменил запись), но там нет версионирования записей и не предусмотрена возможность вытащить запись определенной версии.
Hibernate Envers требует минимум две дополнительных таблицы. Первая таблица необходима для каждой сущности, которая будет поддерживать версионирование. В ней должны быть несколько колонок хранящих необходимую информацию для версионирования. Другая для отслеживания номеров версий, а так же меток времени, когда каждая версия была создана.
Имя первой таблицы строится путем добавления суффикса (указывается в настройках) к базовой таблице. В нашем случае для таблицы Contact_Audit необходимо создать таблицу Contact_Audit_H:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
CREATE TABLE CONTACT_AUDIT_H ( ID INT NOT NULL , FIRST_NAME VARCHAR(60) NOT NULL , LAST_NAME VARCHAR(40) NOT NULL , BIRTH_DATE DATE , VERSION INT NOT NULL DEFAULT 0 , CREATED_BY VARCHAR(20) , CREATED_DATE TIMESTAMP , LAST_MODIFIED_BY VARCHAR(20) , LAST_MODIFIED_DATE TIMESTAMP , AUDIT_REVISION INT NOT NULL , ACTION_TYPE INT , AUDIT_REVISION_END INT , AUDIT_REVISION_END_TS TIMESTAMP , UNIQUE UQ_CONTACT_AUDIT_H_1 (FIRST_NAME, LAST_NAME) , PRIMARY KEY (ID) ); |
Нас интересуют колонки:
1 2 3 4 |
AUDIT_REVISION INT NOT NULL , ACTION_TYPE INT , AUDIT_REVISION_END INT , AUDIT_REVISION_END_TS TIMESTAMP |
*Следующие колонки в этой статье не используются (остались из проекта по отслеживанию версий в Spring Data JPA):
1 2 3 4 |
CREATED_BY VARCHAR(20) , CREATED_DATE TIMESTAMP , LAST_MODIFIED_BY VARCHAR(20) , LAST_MODIFIED_DATE TIMESTAMP |
Вторая необходимая таблица REVINFO (такое название обязательно):
1 2 3 4 5 |
CREATE TABLE REVINFO ( REVTSTMP BIGINT NOT NULL , REV INT NOT NULL AUTO_INCREMENT , PRIMARY KEY (REV) ); |
Результат:
5. Конфигурирование spring-context.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--@Transaction annotation support --> <tx:annotation-driven transaction-manager="transactionManager"/> <!--Обеспечивает работу с транзакциями в Spring --> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="emf"/> </bean> <!-- EntityManagerFactory --> <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource" /> <!--Поставщик данных - hibernate--> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" /> </property> <!--поиск сущностей в этом пакете--> <property name="packagesToScan" value="ru.javastudy"/> <!--детали конфигурации поставщика постоянства (hibernate) --> <property name="jpaProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop> <prop key="hibernate.max_fetch_depth">3</prop> <prop key="hibernate.jdbc.fetch_size">50</prop> <prop key="hibernate.jdbc.batch_size">10</prop> <prop key="hibernate.show_sql">true</prop> <!-- Properties for Hibernate Envers --> <prop key="org.hibernate.envers.audit_table_suffix">_H</prop> <prop key="org.hibernate.envers.revision_field_name">AUDIT_REVISION</prop> <prop key="org.hibernate.envers.revision_type_field_name">ACTION_TYPE</prop> <prop key="org.hibernate.envers.audit_strategy">org.hibernate.envers.strategy.ValidityAuditStrategy</prop> <prop key="org.hibernate.envers.audit_strategy_validity_end_rev_field_name">AUDIT_REVISION_END</prop> <prop key="org.hibernate.envers.audit_strategy_validity_store_revend_timestamp">true</prop> <prop key="org.hibernate.envers.audit_strategy_validity_revend_timestamp_field_name">AUDIT_REVISION_END_TS</prop> </props> </property> </bean> <!-- Datasource. Источник данных - база MySQL --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/javastudy" /> <property name="username" value="root" /> <property name="password" value="admin" /> </bean> <!--Activates various annotations to be detected in bean classes: Spring's @Required and @Autowired and so on--> <context:annotation-config/> <!--Scanning components in base-package (look for annotations) --> <context:component-scan base-package="ru.javastudy"/> </beans> |
Все настройки описаны в статьях о Hibernate и JPA. Для этой статьи добавлены:
1 2 3 4 5 6 7 8 |
<!-- Properties for Hibernate Envers --> <prop key="org.hibernate.envers.audit_table_suffix">_H</prop> <prop key="org.hibernate.envers.revision_field_name">AUDIT_REVISION</prop> <prop key="org.hibernate.envers.revision_type_field_name">ACTION_TYPE</prop> <prop key="org.hibernate.envers.audit_strategy">org.hibernate.envers.strategy.ValidityAuditStrategy</prop> <prop key="org.hibernate.envers.audit_strategy_validity_end_rev_field_name">AUDIT_REVISION_END</prop> <prop key="org.hibernate.envers.audit_strategy_validity_store_revend_timestamp">true</prop> <prop key="org.hibernate.envers.audit_strategy_validity_revend_timestamp_field_name">AUDIT_REVISION_END_TS</prop> |
Подробнее о добавленных свойствах:
- audit_table_suffix — суффикс имени таблицы для сущности, для которой отслеживаются версии. Например, для сущностного класса ContactAuditEntity, который отображается на таблицу CONTACT_AUDIT, Hibernate Envers будет сохранять хронологию в таблице CONTACT_AUDIT_H, поскольку свойство audit_table_suffix установлено в _H. О нем писалось чуть выше при создании таблицы.
- revision_field_name — столбец таблицы хронологии для сохранения номера версии для каждой записи хронологии
- revision_type_field_name — столбец таблицы хронологии для сохранения типа действия обновления
- audit_strategy — стратегия аудита, используемая для отслеживания версий сущностей
- audit_strategy_validity_end_rev_field_name — столбец таблицы хронологии для сохранения номера конечной версии для каждой записи хронологии. Требуется только в случае применения стратегии аудита достоверности
- audit_strategy_validity_store_revend_timestamp — следует ли сохранять метки времени при обновлении номера конечной версии для каждой записи хронологии. Требуется только в случае применения стратегии аудита достоверности
- audit_strategy_validity_revend_timestamp_field_name — столбец таблицы хронологии для сохранения метки времени, когда обновляется номер конечной версии для каждой записи хронологии. Требуется только в случае применения стратегии аудита достоверности и при условии, что предыдущее свойство установлено в true
6. Настройка сущностей для отслеживания версий
Сущность ContactAuditEntity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
package ru.javastudy.entities; import org.hibernate.envers.Audited; import org.hibernate.envers.NotAudited; import javax.persistence.*; import java.io.Serializable; import java.util.Date; import java.util.HashSet; import java.util.Set; @Entity @Audited @Table(name = "contact_audit", schema = "", catalog = "javastudy") public class ContactAuditEntity implements Serializable { private Integer id; private String firstName; private String lastName; private Date birthDate; private int version; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false, insertable = true, updatable = true) public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Basic @Column(name = "first_name", nullable = false, insertable = true, updatable = true, length = 60) public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } @Basic @Column(name = "last_name", nullable = false, insertable = true, updatable = true, length = 40) public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Basic @Temporal(TemporalType.DATE) @Column(name = "birth_date", nullable = true, insertable = true, updatable = true) public Date getBirthDate() { return birthDate; } public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } @Version @Column(name = "version", nullable = false, insertable = true, updatable = true) public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } private Set<HobbyEntity> hobbies = new HashSet<HobbyEntity>(); @ManyToMany @NotAudited @JoinTable(name = "contact_hobby_detail", joinColumns = @JoinColumn(name = "contact_id"), inverseJoinColumns = @JoinColumn(name = "hobby_id")) public Set<HobbyEntity> getHobbies() { return hobbies; } public void setHobbies(Set<HobbyEntity> hobbies) { this.hobbies = hobbies; } private Set<ContactTelDetailEntity> contactTelDetails = new HashSet<ContactTelDetailEntity>(); @OneToMany(mappedBy = "contact", cascade = CascadeType.ALL, orphanRemoval = true) @NotAudited public Set<ContactTelDetailEntity> getContactTelDetails() { return contactTelDetails; } public void setContactTelDetails(Set<ContactTelDetailEntity> contactTelDetails) { this.contactTelDetails = contactTelDetails; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ContactAuditEntity that = (ContactAuditEntity) o; if (id != that.id) return false; if (version != that.version) return false; if (firstName != null ? !firstName.equals(that.firstName) : that.firstName != null) return false; if (lastName != null ? !lastName.equals(that.lastName) : that.lastName != null) return false; if (birthDate != null ? !birthDate.equals(that.birthDate) : that.birthDate != null) return false; return true; } @Override public int hashCode() { int result = id; result = 31 * result + (firstName != null ? firstName.hashCode() : 0); result = 31 * result + (lastName != null ? lastName.hashCode() : 0); result = 31 * result + (birthDate != null ? birthDate.hashCode() : 0); result = 31 * result + version; return result; } @Override public String toString() { return "ContactAuditEntity{" + "id=" + id + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", birthDate=" + birthDate + ", version=" + version + '}'; } } |
Для включения Hibernate Envers используются две аннотации @Audited и @NotAudited. С первой думаю всё понятно, а вторая используется на таблицах ассоциациях (@ManyToMany и @OneToMany), т.к. Hibernate автоматически попытается отследить и версии связанных таблиц. В нашем случае это не требуется и мы поставим аннотацию, запрещающую отслеживать какие-либо изменения в сущности ассоциации.
6.1. Извлечение записей хронологии
Hibernate Envers предоставляет интерфейс org.hibernate.envers.AuditReader, который можно получить из класса AuditReaderFactory.
Интерфейс с методом для поиска записи по номеру ревизии ContactAuditService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package ru.javastudy.intf; import ru.javastudy.entities.ContactAuditEntity; import java.util.List; public interface ContactAuditService { @PersistenceContext private EntityManager entityManager; ContactAuditEntity findById(Integer id); ContactAuditEntity save(ContactAuditEntity contact); //Hibernate Envers ContactAuditEntity findAuditByRevision(Integer id, int revision); |
Реализация интерфейса (код только для Hibernate Envers) ContactAuditServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package ru.javastudy.impl; import org.hibernate.Query; import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.javastudy.entities.ContactAuditEntity; import ru.javastudy.intf.ContactAuditService; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; import java.util.List; @Service("contactAuditService") @Repository @Transactional public class ContactAuditServiceImpl implements ContactAuditService { @PersistenceContext private EntityManager entityManager; @Transactional public ContactAuditEntity findAuditByRevision(Integer id, int revision) { AuditReader auditReader = AuditReaderFactory.get(entityManager); return auditReader.find(ContactAuditEntity.class, id, revision); } } |
Полный листинг ContactAuditServiceImpl :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package ru.javastudy.impl; import org.hibernate.Query; import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.javastudy.entities.ContactAuditEntity; import ru.javastudy.intf.ContactAuditService; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; import java.util.List; @Service("contactAuditService") @Repository @Transactional public class ContactAuditServiceImpl implements ContactAuditService { @PersistenceContext private EntityManager entityManager; public List<ContactAuditEntity> findAll() { return null; } public ContactAuditEntity findById(Integer id) { TypedQuery<ContactAuditEntity> query = entityManager.createQuery( "select distinct c from ContactAuditEntity c left join fetch c.contactTelDetails t left join fetch c.hobbies h where c.id = :id", ContactAuditEntity.class); query.setParameter("id", id); return query.getSingleResult(); } public ContactAuditEntity save(ContactAuditEntity contact) { if (contact.getId() == null) { entityManager.persist(contact); } else { entityManager.merge(contact); } return contact; } @Transactional public ContactAuditEntity findAuditByRevision(Integer id, int revision) { AuditReader auditReader = AuditReaderFactory.get(entityManager); return auditReader.find(ContactAuditEntity.class, id, revision); } } |
7. Тестирование приложения
Класс Main:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
package ru.javastudy.app; import org.springframework.context.support.GenericXmlApplicationContext; import ru.javastudy.entities.ContactAuditEntity; import ru.javastudy.intf.ContactAuditService; import java.util.Date; public class Main { public static void main(String[] args) { GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(); ctx.load("classpath:spring-config.xml"); ctx.refresh(); ContactAuditService auditService = ctx.getBean("contactAuditService", ContactAuditService.class); enversExample(auditService); } public static void enversExample(ContactAuditService service) { ContactAuditEntity contact = new ContactAuditEntity(); contact.setFirstName("FirstNameEnvers"); contact.setLastName("LastNameEnvers"); contact.setBirthDate(new Date()); contact = service.save(contact); //use it for test update and look for revision contact = service.findById(8); //update contact contact.setFirstName("FirstNameEnvers5"); contact.setLastName("LastNameEnvers5"); contact.setBirthDate(new Date()); contact = service.save(contact); ContactAuditEntity contactAuditEntity = service.findAuditByRevision(8, 1); System.out.println(""); System.out.println("revision 1 " + contactAuditEntity); System.out.println(""); contactAuditEntity = service.findAuditByRevision(8, 3); System.out.println(""); System.out.println("revision 3 " + contactAuditEntity); System.out.println(""); } } |
Сначала создаем новый контакт и записываем в таблицу. Затем находим контакт по id и перезаписываем часть информации. Далее находим записи с различной версией.
Вам для тестирования необходимо комментировать часть кода для вставки первого контакта (т.к. будет дубляж в дальнейшем) и оставлять только пересохранение. Так же id и номер версии вам следует указать свой.
Результат:
1 2 3 4 5 |
revision 1 ContactAuditEntity{id=8, firstName='FirstNameEnvers', lastName='LastNameEnvers', birthDate=2015-10-12, version=0} Hibernate: select contactaud0_.id as id1_2_, contactaud0_.AUDIT_REVISION as AUDIT_RE2_2_, contactaud0_.ACTION_TYPE as ACTION_T3_2_, contactaud0_.AUDIT_REVISION_END as AUDIT_RE4_2_, contactaud0_.AUDIT_REVISION_END_TS as AUDIT_RE5_2_, contactaud0_.birth_date as birth_da6_2_, contactaud0_.first_name as first_na7_2_, contactaud0_.last_name as last_nam8_2_ from javastudy.contact_audit_H contactaud0_ where contactaud0_.AUDIT_REVISION<=? and contactaud0_.ACTION_TYPE<>? and contactaud0_.id=? and (contactaud0_.AUDIT_REVISION_END>? or contactaud0_.AUDIT_REVISION_END is null) revision 3 ContactAuditEntity{id=8, firstName='FirstNameEnvers3', lastName='LastNameEnvers3', birthDate=2015-10-12, version=1} |
Как видите для ревизии 1 и 3 был возвращен корректный результат, показывающий различные версии для контакта с id=8.
Можеть быть интересно
Spring Data JPA Auditing + Hibernate Envers — аудит изменения записи и сохранение ее версий
Исходные коды
Hibernate — Envers — src
5