Spring Data JPA — отслеживание изменений в сущностном классе (Spring Data JPA Auditing)
Аудит изменений в сущностном классе с помощью Spring Data JPA Auditing.
Используемые технологии
- Spring 4.1.5.RELEASE
- Spring Data JPA Gosling Release (1.9)
- Hibernate 5.0.1.Final
- JPA 2.1
- MySQL 5.6.25
- IntelliJ IDEA 14
- Maven 3.2.5
1. Описание задачи
Допустим нам необходимо отслеживать любые изменения в сущностном классе и вносить данные о времени создания и изменения класса, имени пользователя, создавшего данные, а так же имя пользователя, который внес последнюю модификацию. Для решения такой задачи Spring Data JPA предлагает функцию в виде слушателя сущностей JPA, который автоматически отслеживает информацию аудита.
2. Структура проекта
Весь проект основан на статье Spring Data JPA – пример приложения Hello World. Здесь будут описываться только необходимые изменения для включения функции аудита. Поэтому рекомендую ознакомиться с указанной статьей, чтобы было понятно о чем идет речь.
В проекте добавлены: ContactAuditEntity — сущность таблицы для тестирования аудита, ContactAuditService — интерфейс для методов вставки, сохранения и обновления, ContactAuditRepository — интерфейс, расширяющий CrudRepository<T, ID>, ContactAuditServiceImpl — реализация интерфейса, AuditorAwareBean — бин для предоставления информации о пользователе, который вносит изменения.
3. Настройки pom.xml
Зависимости в 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 |
<?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> <!-- 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> |
Важно отметить, что для Hibernate версии 4+ используются другие зависимости, чем для версии старше 4.0 (например для Hibernate 3.6). Для JodaTime теперь нужно подключать вот эту библиотеку:
1 2 3 4 5 |
<dependency> <groupId>org.jadira.usertype</groupId> <artifactId>usertype.jodatime</artifactId> <version>2.0.1</version> </dependency> |
Для Hibernate <4.0 была:
1 2 3 4 5 |
<dependency> <groupId>joda-time</groupId> <artifactId>joda-time-hibernate</artifactId> <version>1.3</version> </dependency> |
Но с ней данный проект не запустится!
4. Таблица ContactAudit, создание сущности ContactAuditEntity
Создадим новую таблицу, которая очень похожа на ContactEntity из прошлой статьи, но с добавлением 4 переменных для аудита:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
create table contact_audit ( id INT NOT NULL AUTO_INCREMENT , 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 DATE , last_modified_by VARCHAR(20) , last_modified_date DATE , UNIQUE UQ_CONTACT_AUDIT_1 (first_name, last_name) , PRIMARY KEY (id) ); |
Получим такую схему:
Теперь создадим сущность для новой таблицы:
Листинг 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 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 |
package ru.javastudy.entities; import org.hibernate.annotations.Type; import org.joda.time.DateTime; import org.springframework.data.domain.Auditable; import javax.persistence.*; import java.io.Serializable; import java.util.Date; @Entity @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; } @Basic @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") @Transient @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; } @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 + '}'; } } |
Сущность реализует интерфейс Auditable<U,ID>, который необходим для работы с изменениями в классе сущности (даты создания-изменения и имя пользователя, создавшего-изменившего сущность). Обычно первым параметром туда передается какой-либо класс, имеющий информацию о пользователе (например User.class), но здесь будет передана жестко прописанная строка. Методы Auditable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package org.springframework.data.domain; import java.io.Serializable; import org.joda.time.DateTime; import org.springframework.data.domain.Persistable; public interface Auditable<U, ID extends Serializable> extends Persistable<ID> { U getCreatedBy(); void setCreatedBy(U var1); DateTime getCreatedDate(); void setCreatedDate(DateTime var1); U getLastModifiedBy(); void setLastModifiedBy(U var1); DateTime getLastModifiedDate(); void setLastModifiedDate(DateTime var1); } |
Обратите внимание на метод isNew() в классе сущности (из интерфейса Persistable<ID>) — он будет влиять на определение является ли сущность новой или ее изменяют. Остальные методы работают согласно названиям.
4.1 Дополнительно о Hibernate 4+
Для Hibernate 4+ используем @Type(type=»org.jadira.usertype.dateandtime.joda.PersistentDateTime»)
Для Hibernate <4 — @Type(type=»org.joda.time.contrib.hibernate.PersistentDateTime»), а так же другую библиотеку из описания pom.xml выше.
5. Интерфейсы и реализация классов для операций с БД
interface ContactAuditService — определяет методы поиска и сохранения или обновления записей в таблице.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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); } |
Интерфейс репозиторий ContactAuditRepository. Все необходимые методы уже есть в родительском интерфейсе CrudRepository, поэтому оставляем его пустым:
1 2 3 4 5 6 7 8 9 |
package ru.javastudy.repository; import org.springframework.data.repository.CrudRepository; import ru.javastudy.entities.ContactAuditEntity; public interface ContactAuditRepository extends CrudRepository<ContactAuditEntity, Integer> { //all methods are exist in CrudRepository } |
Реализация интерфейса:
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 |
package ru.javastudy.impl; import com.google.common.collect.Lists; 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 java.util.List; @Service("contactAuditService") @Repository @Transactional public class ContactAuditServiceImpl implements ContactAuditService { @Autowired private ContactAuditRepository contactAuditRepository; 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); } } |
Обратить внимание можно разве что на метод findOne(id) — поиск одной записи по id. Так этот метод назван в CrudRepository.
6. Объявление слушателей AuditingEntityListener<T>
Необходимо объявить слушателей для классов сущностей, чтобы кто-то мог отслеживать изменения в них. Для этого сначала необходимо создать orm.xml настройку (такое имя обязательно — указано в спецификации JPA). Это можно сделать с помощью среды разработки в настройках проекта.
Обратите внимание, что файл в этом проекте находится в resources/META-INF/orm.xml.
Листинг orm.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?xml version="1.0" encoding="UTF-8" ?> <entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd" version="2.0"> <description>JPA</description> <persistence-unit-metadata> <persistence-unit-defaults> <entity-listeners> <entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener" /> </entity-listeners> </persistence-unit-defaults> </persistence-unit-metadata> </entity-mappings> |
6.1. Настройка spring-config.xml
Все настройки взяты из предыдущего проекта. Приведу только изменения, необходимые для аудита:
1 2 3 4 5 |
<!-- Enable auditing in Spring Data --> <jpa:auditing auditor-aware-ref="auditorAwareBean"/> <!-- return user information --> <bean id="auditorAwareBean" class="ru.javastudy.auditor.AuditorAwareBean" /> |
6.2. Бин AuditorAware
В настройках spring указан бин, который реализует необходимый для аудита интерфейс AuditorAware. Листинг AuditorAwareBean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package ru.javastudy.auditor; import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import ru.javastudy.entities.ContactAuditEntity; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; public class AuditorAwareBean implements AuditorAware<String> { public String getCurrentAuditor() { return "byJavaStudyUser"; } } |
Здесь обычно предоставляют данные о пользователе и вместо строки в интерфейс передается что-то вроде User.class. У нас же жестко прописана строчка, которая будет подставляться в колонки CREATED_BY, LAST_MODIFIED_BY.
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 |
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(); ContactService service = ctx.getBean("jpaContactService", ContactService.class); ContactAuditService auditService = ctx.getBean("contactAuditService", ContactAuditService.class); /* Spring Data JPA Audit tutorial*/ auditTutorial(auditService); /* Spring Data JPA Hello World tutorial*/ // helloWorldTutorial(service); } private static void auditTutorial(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); System.out.println("After first save"); contacts = auditService.findAll(); printAll(contacts); //find by id Integer contactId = 1; contact = auditService.findById(contactId); System.out.println("Contact with id: " + contactId + " " + contact); //update System.out.println("Update contact"); contact.setFirstName("updatedName6"); contact.setLastName("updatedLastName6"); auditService.save(contact); contacts = auditService.findAll(); printAll(contacts); } } |
Создаем контакт, вставляем в таблицу. Затем изменяем контакт с id = 1.
Для того чтобы протестировать операцию обновления, закомментируйте начальный код вставки и оставьте только операцию обновления для контакта с нужным id.
8. Проблема со временем обновления
Обратите внимание на время, которое указывается в обновлении:
CREATED_DATE = «2015-10-11 21:06:38» ; LAST_MODIFIED_DATE = «2015-10-11 18:06:38». Решение уже в другой статье.
Может быть интересно
Spring Data JPA Auditing + Hibernate Envers – аудит изменения записи и сохранение ее версий
Hibernate Envers – отслеживание версий (ревизий) класса сущности и извлечение ее хронологии
Исходные коды
Spring Data JPA Audit SQL -sql MySQL
Spring Data JPA — Audit — src
4