Spring Data JPA Auditing + Hibernate Envers — аудит изменения записи и сохранение ее версий
Рассмотрим пример аудита изменений сущности, а так же сохранение каждой её версии.
Используемые технологии
- Spring 4.1.5.RELEASE
- Spring Data JPA Gosling Release (1.9)
- Hibernate 5.0.1.Final ( + Hibernate Envers)
- JPA 2.1
- MySQL 5.6.25
- IntelliJ IDEA 14
- Maven 3.2.5
1. Описание задачи
Необходимо подружить вместе Spring Data JPA Auditing и модуль Hibernate Envers. Первый отвечает за аудит изменений в сущности и пишет параметры: created_by, created_date, last_modified_by, last_modiefied_date (кто и когда создал, кто и когда изменил в последний раз). Hibernate Envers сохраняет версии (ревизии) каждой измененной сущности (для этого нужны еще две дополнительных таблицы), но он позволяет вытащить любую версию, например по id.
2. Структура проекта
В этой статье будут рассмотрены только настройки, которые позволят нам отcлеживать изменения в сущностях. Подробная информация есть в двух базовых статьях по фреймворкам: Spring Data JPA – отслеживание изменений в сущностном классе (Spring Data JPA Auditing) и Hibernate Envers – отслеживание версий (ревизий) класса сущности и извлечение ее хронологии. Если вы не знакомы с этими технологиями, то вначале рекомендую прочитать эти статьи.
Структура осталась такой же, как и для примера аудита в Spring Data JPA. Таблицы выглядят так (что добавлено было описано в статье по Hibernate Envers):
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
<?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>jpa_quickStart</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> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-releasetrain</artifactId> <version>${spring-data-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> <!-- Hibernate --> <!-- for JPA, use hibernate-entitymanager instead of hibernate-core --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate-version}</version> </dependency> <!-- Hibernate Envers--> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> <version>${hibernate-version}</version> </dependency> <!-- Spring Data JPA --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> </dependency> <!--AOP. Need for Spring Data JPA --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </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> <!-- Support methods from google. For example 'Lists.newArrayList()'--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency> </dependencies> </project> |
4. Настройки 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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
<?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" xmlns:jpa="http://www.springframework.org/schema/data/jpa" 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 http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.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"/> <!-- Need for Repository abstraction --> <jpa:repositories base-package="ru.javastudy.repository" entity-manager-factory-ref="emf" transaction-manager-ref="transactionManager"/> <!-- Enable auditing in Spring Data --> <jpa:auditing auditor-aware-ref="auditorAwareBean"/> <!-- return user information --> <bean id="auditorAwareBean" class="ru.javastudy.auditor.AuditorAwareBean" /> </beans> |
5. Листинг сущности для аудита и версионирования
ContactAuditEntity (этот вид окончательно сущность приняла в статье о Hibernate Envers, указанной вначале):
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 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
package ru.javastudy.entities; import org.hibernate.annotations.Type; import org.hibernate.envers.Audited; import org.hibernate.envers.NotAudited; import org.joda.time.DateTime; import org.springframework.data.domain.Auditable; 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 Auditable<String, Integer>, Serializable { private Integer id; private String firstName; private String lastName; private Date birthDate; private int version; private String createdBy; private DateTime createdDate; private String lastModifiedBy; private DateTime lastModifiedDate; @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; } @Basic @Column(name = "created_by", nullable = true, insertable = true, updatable = true, length = 20) public String getCreatedBy() { return createdBy; } public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } @Column(name = "created_date") @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime") public DateTime getCreatedDate() { return createdDate; } public void setCreatedDate(DateTime createdDate) { this.createdDate = createdDate; } @Basic @Column(name = "last_modified_by", nullable = true, insertable = true, updatable = true, length = 20) public String getLastModifiedBy() { return lastModifiedBy; } public void setLastModifiedBy(String lastModifiedBy) { this.lastModifiedBy = lastModifiedBy; } @Column(name = "last_modified_date") @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime") public DateTime getLastModifiedDate() { return lastModifiedDate; } public void setLastModifiedDate(DateTime lastModifiedDate) { this.lastModifiedDate = lastModifiedDate; } @Transient public boolean isNew() { return id == null; } 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; if (createdBy != null ? !createdBy.equals(that.createdBy) : that.createdBy != null) return false; if (createdDate != null ? !createdDate.equals(that.createdDate) : that.createdDate != null) return false; if (lastModifiedBy != null ? !lastModifiedBy.equals(that.lastModifiedBy) : that.lastModifiedBy != null) return false; if (lastModifiedDate != null ? !lastModifiedDate.equals(that.lastModifiedDate) : that.lastModifiedDate != 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; result = 31 * result + (createdBy != null ? createdBy.hashCode() : 0); result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0); result = 31 * result + (lastModifiedBy != null ? lastModifiedBy.hashCode() : 0); result = 31 * result + (lastModifiedDate != null ? lastModifiedDate.hashCode() : 0); return result; } @Override public String toString() { return "ContactAuditEntity{" + "id=" + id + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", birthDate=" + birthDate + ", version=" + version + ", createdBy='" + createdBy + '\'' + ", createdDate=" + createdDate + ", lastModifiedBy='" + lastModifiedBy + '\'' + ", lastModifiedDate=" + lastModifiedDate + '}'; } } |
6. Интерфейс и реализация методов обращения к БД
Интерфейс ContactAuditService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package ru.javastudy.intf; import ru.javastudy.entities.ContactAuditEntity; import java.util.List; public interface ContactAuditService { List<ContactAuditEntity> findAll(); ContactAuditEntity findById(Integer id); ContactAuditEntity save(ContactAuditEntity contact); //Hibernate Envers ContactAuditEntity findAuditByRevision(Integer id, int revision); } |
Его реализация ContactAuditServiceImpl (изменена относительно Hibernate Envers и Spring Data JPA Auditing):
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 |
package ru.javastudy.impl; import com.google.common.collect.Lists; import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; import org.springframework.beans.factory.annotation.Autowired; 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 ru.javastudy.repository.ContactAuditRepository; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; @Service("contactAuditService") @Repository @Transactional public class ContactAuditServiceImpl implements ContactAuditService { @Autowired private ContactAuditRepository contactAuditRepository; @PersistenceContext private EntityManager entityManager; public List<ContactAuditEntity> findAll() { return Lists.newArrayList(contactAuditRepository.findAll()); } public ContactAuditEntity findById(Integer id) { return contactAuditRepository.findOne(id); } public ContactAuditEntity save(ContactAuditEntity contact) { return contactAuditRepository.save(contact); } //Hibernate Envers public ContactAuditEntity findAuditByRevision(Integer id, int revision) { AuditReader reader = AuditReaderFactory.get(entityManager); return reader.find(ContactAuditEntity.class, id, revision); } } |
7. Тестирование
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 |
package ru.javastudy.app; import org.springframework.context.support.GenericXmlApplicationContext; import ru.javastudy.entities.ContactAuditEntity; import ru.javastudy.intf.ContactAuditService; import java.util.Date; import java.util.List; public class Main { public static void main(String[] args) { GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(); ctx.load("classpath:spring-config.xml"); //move from src.main.java to src.main.resources ctx.refresh(); ContactAuditService auditService = ctx.getBean("contactAuditService", ContactAuditService.class); /* Spring Data JPA Audit +Hibernate Envers tutorial*/ auditAndVersionTutorial(auditService); } private static void auditAndVersionTutorial(ContactAuditService auditService) { List<ContactAuditEntity> contacts = auditService.findAll(); printAll(contacts); //Add new contact ContactAuditEntity contact = new ContactAuditEntity(); contact.setFirstName("auditFirstName"); contact.setLastName("auditLastName"); contact.setBirthDate(new Date()); //save contact System.out.println("before save"); auditService.save(contact); //find by id Integer contactId = 11; contact = auditService.findById(contactId); System.out.println("Contact with id: " + contactId + " " + contact); //update System.out.println("Update contact"); contact.setFirstName("updatedNameEnversMore4"); contact.setLastName("updatedLastNameEnversMore4"); auditService.save(contact); contacts = auditService.findAll(); printAll(contacts); ContactAuditEntity contactAuditEntity = auditService.findAuditByRevision(contactId, 11); System.out.println(""); System.out.println("revision 11 " + contactAuditEntity); System.out.println(""); contactAuditEntity = auditService.findAuditByRevision(contactId, 13); System.out.println(""); System.out.println("revision 13 " + contactAuditEntity); System.out.println(""); } private static <T> void printAll(List<T> contacts) { System.out.println("printAll: "); for (T contact : contacts) { System.out.println(contact); } } } |
Обращаю ваше внимание, что если запускать код именно так, то будут вылазить ошибки или дубликатов записей, отсутствия записи и т.п.. Вам нужно комментировать часть кода (например после первой вставки выключить первый вызов save() ). Так же смотрите на id и номер ревизии, который вызывается в конце этого кода.
Примерный вывод:
1 2 3 4 |
revision 11 ContactAuditEntity{id=11, firstName='updatedNameEnvers', lastName='updatedLastNameEnvers', birthDate=2015-10-13, version=0, createdBy='byJavaStudyUser', createdDate=2015-10-14T02:23:31.000+03:00, lastModifiedBy='byJavaStudyUser', lastModifiedDate=2015-10-13T23:22:59.000+03:00} revision 13 ContactAuditEntity{id=11, firstName='updatedNameEnversMore4', lastName='updatedLastNameEnversMore4', birthDate=2015-10-13, version=0, createdBy='byJavaStudyUser', createdDate=2015-10-14T02:46:47.000+03:00, lastModifiedBy='byJavaStudyUser', lastModifiedDate=2015-10-13T23:24:04.000+03:00} |
Как видите сохранилась вся необходимая информация об изменениях в записи, а так же доступны различные ревизии (номера 11 и 13).
Исходные коды
Spring Data JPA Audit Hibernate Envers SQL -sql
Spring Data JPA Audit +Hibernate Envers — src
3