Skip to content

Аудит и отслеживание версий для сущностных классов в Spring

Синопсис

Аудит и отслеживание версий бизнес данных применяется в более или менее серьёзных корпоративных приложениях. Так как такие сущности как поставщик, склад и продукция являются основными бизнес объектами манипулирования в нашем приложении, то может возникнуть потребность за ними наблюдать, другими словами может потребоваться иметь информацию о том кто изменил состояние объекта (это называется аудит) а также какое состояние этот объект имел до этого изменения или какое состояние у этого объекта было несколько изменений назад (это называется версионированием), то есть мы хотим держать всю систему под колпаком. В этом посте мы рассмотрим два этих аспекта (аудит и версионирование сущностных классов) на пример нашего приложения по работе со складской базой данных Warehouse.

1

Похожие посты

  • Работа с базой данных через JDBC
  • Работа с базой данных через Spring
  • Hibernate (конфигурация в стиле xml)
  • Hibernate (конфигурация в стиле аннотации)
  • Hibernate (конфигурация spring)
  • Основы JPA
  • JPA и Spring
  • Простой Spring MVC

  • Весь проект можно взять с гитхаба: https://github.com/dev-blogs/jpa под проект simple-JPA-HibernateEnvers

    Немного теории

    Основную работу по реализации аудита и версионированию берет на себя хибернейтовский модуль Hibernate Envers — (Entity Versioning System). От нас только требуется предоставить модулю Hibernate Envers нужную структуру базы данных чтобы он мог сохранять изменяемые объекты предметной области в хронологическом порядке и прописать соответствующие свойства в конфигурационном спринговом файле, чтобы Hibernate Envers знал как называются таблицы хронологии.
    В Hibernate Envers существует два подхода аудита, это дефолтовый и Аудит достоверности. На дефолтовом подходе я останавливаться не буду, так как особо в него не вникал, поэтому расскажу только про аудит достоверности.
    Идея аудита достоверности такая, каждый раз, когда мы добавляем или изменяем отслеживаемую запись, допустим информацию о поставщике, то с каждым изменением информации о поставщике, в таблицу хронологии добавляется новая запись с номером текущей ревизии (версии), и в то же самое время (то есть в той же транзакции) в предыдущей записи (то есть в записи о предыдущем состоянии поставщика) изменяется поле ревизии состоянии в которую этот поставщик перешел, то есть в следующую.
    Объясню теперь на примере, для краткости я не буду приводить эс-кью-эль код обновления этих таблиц, а приведу только результаты обновлений. Допустим у нас есть поставщик по имени New Provider:

    Таблица PROVIDERS (состояние 1)

    +----+--------------+------------+---------------------+------------------+---------------------+
    | id | name         | created_by | created_date        | last_modified_by | last_modified_date  |
    +----+--------------+------------+---------------------+------------------+---------------------+
    |  6 | New Provider | username   | 2016-07-13 18:57:00 | username         | 2016-07-13 19:17:59 |
    +----+--------------+------------+---------------------+------------------+---------------------+
    

    теперь мы хотим поменять ему нэйм на Update provider:

    Таблица PROVIDERS (состояние 2)

    +----+-----------------+------------+---------------------+------------------+---------------------+
    | id | name            | created_by | created_date        | last_modified_by | last_modified_date  |
    +----+-----------------+------------+---------------------+------------------+---------------------+
    |  6 | Update provider | username   | 2016-07-13 18:57:00 | username         | 2016-07-13 19:18:01 |
    +----+-----------------+------------+---------------------+------------------+---------------------+
    

    теперь мы хотим поменять нэйм на test1:

    Таблица PROVIDERS (состояние 3)

    +----+-------+------------+---------------------+------------------+---------------------+
    | id | name  | created_by | created_date        | last_modified_by | last_modified_date  |
    +----+-------+------------+---------------------+------------------+---------------------+
    |  6 | test1 | username   | 2016-07-13 18:57:00 | username         | 2016-07-13 19:18:03 |
    +----+-------+------------+---------------------+------------------+---------------------+
    

    и на test2:

    Таблица PROVIDERS (состояние 4)

    +----+-------+------------+---------------------+------------------+---------------------+
    | id | name  | created_by | created_date        | last_modified_by | last_modified_date  |
    +----+-------+------------+---------------------+------------------+---------------------+
    |  6 | test2 | username   | 2016-07-13 18:57:00 | username         | 2016-07-13 19:18:05 |
    +----+-------+------------+---------------------+------------------+---------------------+
    

    И так не считая создания поставщика у нас произошло с объектом поставщика всего 3 изменения. Все эти изменения зафиксировались в таблице хронологии. Вот как выглядит таблица хронологии где отображена история изменений поставщика (чтобы таблица уместилась полностью я её немного урезал, некоторые поля выкинул, некоторые сократил):

    Таблица PROVIDERS_CHRONOLOGY

    +--+---------------+------------+-------------------+--------------+-----------+------------------+---------------------+
    |id| name          |created_date|last_modified_date |audit_revision|action_type|audit_revision_end|audit_revision_end_ts|
    +--+---------------+------------+-------------------+--------------+-----------+------------------+---------------------+
    | 6|New Provider   | 19:18:01   |2016-07-13 19:17:59|            1 |         0 |                2 | 2016-07-13 19:18:01 |
    | 6|Update provider| 19:18:03   |2016-07-13 19:18:01|            2 |         1 |                3 | 2016-07-13 19:18:03 |
    | 6|test1          | 19:18:05   |2016-07-13 19:18:03|            3 |         1 |                4 | 2016-07-13 19:18:05 |
    | 6|test2          | 19:17:59   |2016-07-13 19:18:05|            4 |         1 |             NULL | 2016-07-13 19:18:05 |
    +--+---------------+------------+-------------------+--------------+-----------+------------------+---------------------+
    

    Обратим внимание на столбцы audit_revision и audit_revision_end из таблици хронологии, это метки ревизий для отдельно взятого состояния поставщика. Столбец audit_revision означает ту ревизию, в которой он приобрел соответствующее состояние, а столбец audit_revision_end означает принимающую ревизию, то есть ту ревизию в которой текущее состояние обновилось на другое. Ну то есть в ревизии 2 у поставщика было состояние Update provider, а ревизия в которой состояние Update provider было изменено на test1, отображается в поле audit_revision_end которое соответствует ревизии 3. Это конечно логично, что после 2 идёт 3, но это потому что пример простой, на практике между двумя изменениями объекта может быть много ревизий не касающиеся текущего объекта. Точно так же обстоит дело со столбцами last_modified_date и audit_revision_end_ts. Поле last_modified_date это дата создания ревизии, то есть момент изменения состояния поставщика для текущей ревизии, а столбец audit_revision_end_ts означает дата перехода поставщика в следующее состояние.

    Для последней ревизии, то есть для ревизии номер 4 поле audit_revision_end равно NULL так как для четвертой ревизии ещё нету принимающей ревизии, а поле audit_revision_end_ts равно полю last_modified_date.

    Еще стоит обратить внимание в таблице хронологии на столбец action_type. Это тип операции над объектом. Здесь операция 0 означает создание нового поставщика, а операция 1 изменение состояния существующего поставщика.

    Сама таблица ревизий выглядит так:

    Таблица REVINFO

    +-----+---------------+
    | rev | revtstmp      |
    +-----+---------------+
    |   1 | 1468426679204 |
    |   2 | 1468426681322 |
    |   3 | 1468426683393 |
    |   4 | 1468426685448 |
    +-----+---------------+
    

    В таблице ревизий столбец rev означает номер ревизии, а столбец revtstmp метка времени — момент ревизии в миллисекундах.

    Структура проекта

    simple-JPA-HibernateEnvers
        ├──src
        │   ├─main
        │   │   └─java
        │   │       └─com
        │   │           └─devblogs
        │   │               ├─App.java
        │   │               ├─auditor
        │   │               │   └─AuditorAwareBean.java
        │   │               ├─service
        │   │               │   ├─ItemSerivce.java
        │   │               │   ├─ProviderService.java
        │   │               │   ├─WarehouseService.java
        │   │               │   └─jpa
        │   │               │       ├─ItemSerivceImpl.java
        │   │               │       ├─ProviderSerivceImpl.java
        │   │               │       └─WarehouseSerivceImpl.java
        │   │               └─model
        │   │                   ├─Item.java
        │   │                   ├─Provider.java
        │   │                   └─Warehouse.java
        │   └─resources
        │       ├─META-INF
        │       │   └─orm.xml
        │       ├─spring
        │       │   └─spring-data-app-context.xml
        │       ├─scripts
        │       │   ├─create-data-model.sql
        │       │   └─fill-database.sql
        │       └─log4j.properties
        └──pom.xml
    

    Структура базы данных Warehouse

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

    create-data-model.sql

    DROP DATABASE IF EXISTS warehouse;
    CREATE DATABASE warehouse CHARACTER SET utf8; 
    USE warehouse;
    
    CREATE TABLE warehouses (
        id INT UNSIGNED NOT NULL AUTO_INCREMENT,
        address VARCHAR(255) NOT NULL,
        created_by VARCHAR(255),
        created_date TIMESTAMP,
        last_modified_by VARCHAR(255),
        last_modified_date TIMESTAMP,    
        UNIQUE UQ_ADDRESS_1 (address),
        PRIMARY KEY (id)
    );
     
    CREATE TABLE items (
        id INT UNSIGNED NOT NULL AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        created_by VARCHAR(255),
        created_date TIMESTAMP,
        last_modified_by VARCHAR(255),
        last_modified_date TIMESTAMP,
        warehouse_id INT UNSIGNED,
        PRIMARY KEY (id),
        FOREIGN KEY (warehouse_id) REFERENCES warehouses (id)
            ON DELETE CASCADE
            ON UPDATE CASCADE
    );
     
    CREATE TABLE providers (
        id INT UNSIGNED NOT NULL AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        created_by VARCHAR(255),
        created_date TIMESTAMP,
        last_modified_by VARCHAR(255),
        last_modified_date TIMESTAMP,
        PRIMARY KEY (id)
    );
    
    CREATE TABLE warehouses_chronology (
        id INT UNSIGNED NOT NULL AUTO_INCREMENT,
        address VARCHAR(255) NOT NULL,
        created_by VARCHAR(255),
        created_date TIMESTAMP,
        last_modified_by VARCHAR(255),
        last_modified_date TIMESTAMP,
        audit_revision INT NOT NULL,
        action_type int,
        audit_revision_end int,
        audit_revision_end_ts TIMESTAMP,    
        UNIQUE UQ_ADDRESS_CHRONOLOGY_1 (address),
        PRIMARY KEY (id, audit_revision)
    );
    
    CREATE TABLE items_chronology (
        id INT UNSIGNED NOT NULL AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        created_by VARCHAR(255),
        created_date TIMESTAMP,
        last_modified_by VARCHAR(255),
        last_modified_date TIMESTAMP,    
        audit_revision INT NOT NULL,
        action_type int,
        audit_revision_end int,
        audit_revision_end_ts TIMESTAMP,
        warehouse_id INT UNSIGNED,
        PRIMARY KEY (id, audit_revision),
        FOREIGN KEY (warehouse_id) REFERENCES warehouses (id)
            ON DELETE CASCADE
            ON UPDATE CASCADE
    );
    
    CREATE TABLE providers_chronology (
    	id INT UNSIGNED NOT NULL AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        created_by VARCHAR(255),
        created_date TIMESTAMP,
        last_modified_by VARCHAR(255),
        last_modified_date TIMESTAMP,
        audit_revision INT NOT NULL,
        action_type int,
        audit_revision_end int,
        audit_revision_end_ts TIMESTAMP,
        PRIMARY KEY (id, audit_revision)
    );
     
    CREATE TABLE items_providers (
        item_id INT UNSIGNED NOT NULL,
        provider_id INT UNSIGNED NOT NULL,
        FOREIGN KEY (item_id) REFERENCES items (id)
            ON DELETE CASCADE
            ON UPDATE CASCADE,
        FOREIGN KEY (provider_id) REFERENCES providers (id)
            ON DELETE CASCADE
            ON UPDATE CASCADE
    );
    
    CREATE TABLE REVINFO (
    	rev INT NOT NULL AUTO_INCREMENT,
    	revtstmp BIGINT NOT NULL,	
    	PRIMARY KEY (rev, revtstmp)
    );
    

    Заполним все таблицы данными. Таблицы хронологии и ревизии не трогаем, они будут заполняться Hibernate Envers:

    fill-database.sql

    USE warehouse;
    
    INSERT INTO warehouses (address) VALUES ('ul. one, 5');
    INSERT INTO warehouses (address) VALUES ('ul. two, 4');
    INSERT INTO warehouses (address) VALUES ('ul. three, 7');
     
    INSERT INTO items (name, warehouse_id) VALUES ('red table', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('blue table', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('green table', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('black chair', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('red chair', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('blue chair', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('green chair', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('yellow chair', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('white chair', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('black plates', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('green plates', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('red plates', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('yellow plates', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('grey plates', 1);
    INSERT INTO items (name, warehouse_id) VALUES ('large scissors', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('small scissors', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('red spoon', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('grey spoon', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('green spoon', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('yellow spoon', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('white spoon', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('yellow fork', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('red fork', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('black fork', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('green fork', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('blue fork', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('brown fork', 2);
    INSERT INTO items (name, warehouse_id) VALUES ('yellow blinds', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('red blinds', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('green blinds', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('black blinds', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('white blinds', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('brown blinds', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('grey sofa', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('red sofa', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('yellow sofa', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('black sofa', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('white sofa', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('white cupboard', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('red cupboard', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('blue cupboard', 3);
    INSERT INTO items (name, warehouse_id) VALUES ('green cupboard', 3);
     
    INSERT INTO providers (name) VALUES ('Vasya');
    INSERT INTO providers (name) VALUES ('Petya');
    INSERT INTO providers (name) VALUES ('Sasha');
    INSERT INTO providers (name) VALUES ('Anna');
    INSERT INTO providers (name) VALUES ('Lena');
     
    INSERT INTO items_providers (item_id, provider_id) VALUES (1, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (2, 2);
    INSERT INTO items_providers (item_id, provider_id) VALUES (3, 2);
    INSERT INTO items_providers (item_id, provider_id) VALUES (4, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (5, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (6, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (7, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (8, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (9, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (10, 2);
    INSERT INTO items_providers (item_id, provider_id) VALUES (11, 2);
    INSERT INTO items_providers (item_id, provider_id) VALUES (12, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (13, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (14, 2);
    INSERT INTO items_providers (item_id, provider_id) VALUES (15, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (16, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (17, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (18, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (19, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (20, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (21, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (22, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (23, 2);
    INSERT INTO items_providers (item_id, provider_id) VALUES (24, 2);
    INSERT INTO items_providers (item_id, provider_id) VALUES (25, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (26, 2);
    INSERT INTO items_providers (item_id, provider_id) VALUES (27, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (28, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (29, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (30, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (31, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (32, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (33, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (34, 2);
    INSERT INTO items_providers (item_id, provider_id) VALUES (35, 2);
    INSERT INTO items_providers (item_id, provider_id) VALUES (36, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (37, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (38, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (39, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (40, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (41, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (42, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (1, 3);
    INSERT INTO items_providers (item_id, provider_id) VALUES (7, 1);
    INSERT INTO items_providers (item_id, provider_id) VALUES (7, 2);
    

    Будем использовать СУРБ MySQL в которой нам нужно просетить эту базу. Делается это так:

    mysql -u root -p < create-data-model.sql
    mysql -u root -p < fill-database.sql
    

    Объекты предметной области

    Чтобы сущностный класс стал отслеживаемым Hibernate Envers его необходимо проаннотировать аннотацией @Audited (строчка 26). По умолчанию модуль Hibernate Envers отслеживает все поля, в том числе поля ассоциации. Чтобы упростить пример мы запретим отслеживать сущности ассоции добавив над каждой ассоциацией аннотацию @NotAudited (строчки 108 и 121).

    Кроме того как отслеживать историю изменений сущностного объекта мы ещё будем отслеживать текущие изменения и записывать их в самом сущностном классе это называется аудит объекта которая включает имя пользователя создавшего данные, дату создания, дату последней модификации, а так же имя пользователя который провёл эту модификацию. Для этого сущностный класс реализует метод setCreateBy(), setCreated(), setLastModifiedBy() и setLastModified() из интерфейса org.springframework.data.domain.Auditable из проекта Spring Data JPA (строчки 28, 66-68, 76-78, 85-87, 95-97) и метод isNew() из интерфейса org.springframework.data.domain.Persistable который является пэрэнтом для интерфейса org.springframework.data.domain.Auditable (строчки 100-106). Метод isNew() используется Spring Data JPA для идентификации того, является ли сущность новой.

    Item.java

    package com.devblogs.model;
     
    import java.io.Serializable;
    import java.util.HashSet;
    import java.util.Set;
    import javax.persistence.CascadeType;
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.FetchType;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.JoinColumn;
    import javax.persistence.JoinTable;
    import javax.persistence.ManyToMany;
    import javax.persistence.ManyToOne;
    import javax.persistence.Table;
    import javax.persistence.Transient;
    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;
     
    @Entity
    @Audited
    @Table(name = "items")
    public class Item implements Auditable<String, Long>, Serializable {
        private Long id;
        private String name;
        private String createdBy;
        private DateTime createdDate;
        private String lastModifiedBy;
        private DateTime lastModifiedDate;
        private Set<Provider> providers = new HashSet<Provider>();
        private Warehouse warehouse;
          
        public Item() {
        }
        
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @Column(name = "id")
        public Long getId() {
            return id;
        }
      
        public void setId(Long id) {
            this.id = id;
        }
    
        @Column(name = "name")
        public String getName() {
            return name;
        }
      
        public void setName(String name) {
            this.name = name;
        }
        
        @Column(name = "created_by")
        public String getCreatedBy() {
    		return createdBy;
    	}
    
    	public void setCreatedBy(String createdBy) {
    		this.createdBy = createdBy;
    	}
    
    	@Column(name = "created_date")
    	@Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
    	public DateTime getCreatedDate() {
    		return createdDate;
    	}
    
    	public void setCreatedDate(DateTime createdDate) {
    		this.createdDate = createdDate;
    	}
    
    	@Column(name = "last_modified_by")
    	public String getLastModifiedBy() {
    		return lastModifiedBy;
    	}
    
    	public void setLastModifiedBy(String lastModifiedBy) {
    		this.lastModifiedBy = lastModifiedBy;
    	}
    
    	@Column(name = "last_modified_date")
    	@Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
    	public DateTime getLastModifiedDate() {
    		return lastModifiedDate;
    	}
    
    	public void setLastModifiedDate(DateTime lastModifiedDate) {
    		this.lastModifiedDate = lastModifiedDate;
    	}
    	
    	@Transient
        public boolean isNew() {
    		if (id == null) {
    			return true;
    		} else {
    			return false;
    		}
    	}
    
    	@NotAudited
        @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL})
        @JoinTable(name = "items_providers",
                    joinColumns={@JoinColumn(name = "item_id")},
                    inverseJoinColumns={@JoinColumn(name = "provider_id")})
        public Set<Provider> getProviders() {
            return providers;
        }
      
        public void setProviders(Set<Provider> providers) {
            this.providers = providers;
        }
    
        @NotAudited
        @ManyToOne
        @JoinColumn(name = "warehouse_id")
        public Warehouse getWarehouse() {
            return warehouse;
        }
      
        public void setWarehouse(Warehouse warehouse) {
            this.warehouse = warehouse;
        }
         
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            Item that = (Item) obj;
            if (!name.equals(that.name)) return false;
              
            return true;
        }
        
        @Override
        public String toString() {
            return "\n[id: " + this.id + ",\nname: " + this.name + ",\nCreated by: " +
            		this.createdBy + ",\ncreateDate: " + this.createdDate + ",\nlastModifiedBy: " +
            		this.lastModifiedBy + ",\nlastModifiedDate: " + this.lastModifiedDate + "]";
        }
    }
    

    Provider.java

    package com.devblogs.model;
     
    import java.io.Serializable;
    import java.util.HashSet;
    import java.util.Set;
    import javax.persistence.CascadeType;
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.FetchType;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.JoinColumn;
    import javax.persistence.JoinTable;
    import javax.persistence.ManyToMany;
    import javax.persistence.NamedQueries;
    import javax.persistence.NamedQuery;
    import javax.persistence.Table;
    import javax.persistence.Transient;
    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;
      
    @Entity
    @Audited
    @Table(name = "providers")
    @NamedQueries({
    	@NamedQuery(name = "Provider.findAll", query = "select p from Provider p"),
    	@NamedQuery(name = "Provider.findById", query = "select distinct p from Provider p where p.id = :id")
    })
    public class Provider implements Auditable<String, Long>, Serializable {
        private Long id;
        private String name;
        private String createdBy;
        private DateTime createdDate;
        private String lastModifiedBy;
        private DateTime lastModifiedDate;
        private Set<Item> items = new HashSet<Item>();
          
        public Provider() {   
        }
      
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @Column(name = "id")
        public Long getId() {
            return id;
        }
      
        public void setId(Long id) {
            this.id = id;
        }
      
        @Column(name = "name")
        public String getName() {
            return name;
        }
      
        public void setName(String name) {
            this.name = name;
        }
        
        @Column(name = "created_by")
        public String getCreatedBy() {
    		return createdBy;
    	}
    
    	public void setCreatedBy(String createdBy) {
    		this.createdBy = createdBy;
    	}
    
    	@Column(name = "created_date")
    	@Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
    	public DateTime getCreatedDate() {
    		return createdDate;
    	}
    
    	public void setCreatedDate(DateTime createdDate) {
    		this.createdDate = createdDate;
    	}
    
    	@Column(name = "last_modified_by")
    	public String getLastModifiedBy() {
    		return lastModifiedBy;
    	}
    
    	public void setLastModifiedBy(String lastModifiedBy) {
    		this.lastModifiedBy = lastModifiedBy;
    	}
    
    	@Column(name = "last_modified_date")
    	@Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
    	public DateTime getLastModifiedDate() {
    		return lastModifiedDate;
    	}
    
    	public void setLastModifiedDate(DateTime lastModifiedDate) {
    		this.lastModifiedDate = lastModifiedDate;
    	}
    	
    	@Transient
        public boolean isNew() {
    		if (id == null) {
    			return true;
    		} else {
    			return false;
    		}
    	}
      
    	@NotAudited
        @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL})
        @JoinTable(name = "items_providers",
                joinColumns={@JoinColumn(name = "provider_id")},
                inverseJoinColumns={@JoinColumn(name = "item_id")})
        public Set<Item> getItems() {
            return items;
        }
      
        public void setItems(Set<Item> items) {
            this.items = items;
        }
        
    	@Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            Provider that = (Provider) obj;
            if (!name.equals(that.name)) return false;
            return true;
        }
         
        @Override
        public String toString() {
            return "\n[id: " + this.id + ",\nname: " + this.name + ",\nCreated by: " +
            		this.createdBy + ",\ncreateDate: " + this.createdDate + ",\nlastModifiedBy: " +
            		this.lastModifiedBy + ",\nlastModifiedDate: " + this.lastModifiedDate + "]";
        }
    }
    

    Warehouse.java

    package com.devblogs.model;
     
    import java.io.Serializable;
    import java.util.HashSet;
    import java.util.Set;
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.FetchType;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.OneToMany;
    import javax.persistence.Table;
    import javax.persistence.Transient;
    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;
     
    @Entity
    @Audited
    @Table(name = "warehouses")
    public class Warehouse implements Auditable<String, Long>, Serializable {
        private Long id;
        private String address;
        private String createdBy;
        private DateTime createdDate;
        private String lastModifiedBy;
        private DateTime lastModifiedDate;
        private Set<Item> items = new HashSet<Item>();
     
        public Warehouse() {
        }
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @Column(name = "id")
        public Long getId() {
            return id;
        }
     
        public void setId(Long id) {
            this.id = id;
        }
        
        @Column(name = "address")
        public String getAddress() {
            return address;
        }
     
        public void setAddress(String address) {
            this.address = address;
        }
        
        @Column(name = "created_by")
        public String getCreatedBy() {
    		return createdBy;
    	}
    
    	public void setCreatedBy(String createdBy) {
    		this.createdBy = createdBy;
    	}
    
    	@Column(name = "created_date")
    	@Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
    	public DateTime getCreatedDate() {
    		return createdDate;
    	}
    
    	public void setCreatedDate(DateTime createdDate) {
    		this.createdDate = createdDate;
    	}
    
    	@Column(name = "last_modified_by")
    	public String getLastModifiedBy() {
    		return lastModifiedBy;
    	}
    
    	public void setLastModifiedBy(String lastModifiedBy) {
    		this.lastModifiedBy = lastModifiedBy;
    	}
    
    	@Column(name = "last_modified_date")
    	@Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
    	public DateTime getLastModifiedDate() {
    		return lastModifiedDate;
    	}
    
    	public void setLastModifiedDate(DateTime lastModifiedDate) {
    		this.lastModifiedDate = lastModifiedDate;
    	}
    	
    	@Transient
        public boolean isNew() {
    		if (id == null) {
    			return true;
    		} else {
    			return false;
    		}
    	}
    
    	@NotAudited
        @OneToMany(fetch = FetchType.LAZY, mappedBy = "warehouse")
        public Set<Item> getItems() {
            return items;
        }
     
        public void setItems(Set<Item> items) {
            this.items = items;
        }
         
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            Warehouse that = (Warehouse) obj;
            if (!address.equals(that.address)) return false;
            return true;
        }
         
        @Override
        public String toString() {
            return "\n[id: " + this.id + ",\naddress: " + this.address + ",\nCreated by: " +
            		this.createdBy + ",\ncreateDate: " + this.createdDate + ",\nlastModifiedBy: " +
            		this.lastModifiedBy + ",\nlastModifiedDate: " + this.lastModifiedDate + "]";
        }
    }
    

    Сервисный слой. Интерфейсы

    Теперь мы подошли к самому интересному, после того как у нас есть история изменений объекта, что мы с этой историей можем сделать? В первую очередь мы можем просмотреть историю изменения объекта, но это не так интересно, а ещё мы можем откатить состояние объекта до какой-то ревизии, это делается с помощью интерфейса org.hibernate.envers.AuditReader. Для этого добавим в каждый сервисный интерфейс метод, который будет извлекать из истории нужное состояние соответствующего сущностного объекта по номеру ревизии, назовем его findAuditByRevision (строчки 11):

    ItemService.java

    package com.devblogs.service;
    
    import java.util.List;
    import com.devblogs.model.Item;
    
    public interface ItemService {
    	public List<Item> findAll();
    	public Item findById(Long id);
    	public Item save(Item item);
    	public void delete(Item item);
    	public Item findAuditByRevision(Long id, int revision);
    }
    

    ProviderService.java

    package com.devblogs.service;
    
    import java.util.List;
    import com.devblogs.model.Provider;
    
    public interface ProviderService {
    	public List<Provider> findAll();
    	public Provider findById(Long id);
    	public Provider save(Provider provier);
    	public void delete(Provider provider);
    	public Provider findAuditByRevision(Long id, int revision);
    }
    

    WarehouseService.java

    package com.devblogs.service;
    
    import java.util.List;
    import com.devblogs.model.Warehouse;
    
    public interface WarehouseService {
    	public List<Warehouse> findAll();
    	public Warehouse findById(Long id);
    	public Warehouse save(Warehouse warehouse);
    	public void delete(Warehouse warehouse);
    	public Warehouse findAuditByRevision(Long id, int revision);
    }
    

    Сервисный слой. Имплементация

    Интерфейс org.hibernate.envers.AuditReader запрашивается из класса org.hibernate.envers.AuditReaderFactory. Для этого мы передаём объект класса javax.persistence.EntityManager в класс org.hibernate.envers.AuditReaderFactory получаем объект реализующий интерфейс org.hibernate.envers.AuditReader и запрашиваем у полученного объекта по методу find экземпляр сущности нужной ревизии (строчки 56-60).

    ItemServiceImpl.java

    package com.devblogs.service.impl;
    
    import java.util.List;
    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;
    import javax.persistence.TypedQuery;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.hibernate.envers.AuditReader;
    import org.hibernate.envers.AuditReaderFactory;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import com.devblogs.model.Item;
    import com.devblogs.service.ItemService;
    
    @Service("itemService")
    @Transactional
    public class ItemServiceImpl implements ItemService {
    	private Log log = LogFactory.getLog(ItemServiceImpl.class);
    	
    	@PersistenceContext
    	private EntityManager em;
    
    	@Transactional(readOnly = true)
    	public List<Item> findAll() {
    		return em.createQuery("from Item i", Item.class).getResultList();
    	}
    
    	@Transactional(readOnly = true)
    	public Item findById(Long id) {
    		TypedQuery<Item> typedQuery = em.createQuery("from Item i where i.id = :id", Item.class);
    		typedQuery.setParameter("id", id);
    		return typedQuery.getSingleResult();
    	}
    
    	@Transactional(readOnly = false)
    	public Item save(Item item) {
    		if (item.getId() == null) {
    			log.info("Inserting new item");
    			em.persist(item);
    		} else {
    			em.merge(item);
    			log.info("Updating existing item");
    		}
    		log.info("Item saved with id: " + item.getId());
    		return item;
    	}
    
    	@Transactional(readOnly = false)
    	public void delete(Item item) {
    		Item mergedItem = em.merge(item);
    		em.remove(mergedItem);
    		log.info("Item with id: " + item.getId() + " deleted successfully");
    	}
    
    	@Transactional(readOnly = true)
    	public Item findAuditByRevision(Long id, int revision) {
    		AuditReader auditReader = AuditReaderFactory.get(em);
    		return auditReader.find(Item.class, id, revision);
    	}
    }
    

    ProviderServiceImpl.java

    package com.devblogs.service.impl;
    
    import java.util.List;
    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;
    import javax.persistence.TypedQuery;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    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 com.devblogs.model.Provider;
    import com.devblogs.service.ProviderService;
    
    @Service("providerService")
    @Transactional
    public class ProviderServiceImpl implements ProviderService {
    	private Log log = LogFactory.getLog(ProviderServiceImpl.class);
    	
    	@PersistenceContext	
    	private EntityManager em;
    
    	@Transactional(readOnly = true)
    	public List<Provider> findAll() {
    		return em.createNamedQuery("Provider.findAll", Provider.class).getResultList();
    	}	
    
    	@Transactional(readOnly = true)
    	public Provider findById(Long id) {
    		TypedQuery<Provider> query = em.createNamedQuery("Provider.findById", Provider.class);
    		query.setParameter("id", id);
    		Provider provider = query.getSingleResult();
    		return provider;
    	}
    
    	@Transactional(readOnly = false)
    	public Provider save(Provider provider) {
    		if (provider.getId() == null) {
    			log.info("Inserting new provider");
    			em.persist(provider);
    		} else {
    			em.merge(provider);
    			log.info("Updating existing provider");
    		}
    		log.info("Provider saved with id: " + provider.getId());
    		return provider;
    	}
    
    	@Transactional(readOnly = false)
    	public void delete(Provider provider) {
    		Provider mergedProvider = em.merge(provider);
    		em.remove(mergedProvider);
    		log.info("Provider with id: " + provider.getId() + " deleted successfully");
    	}
    
    	@Transactional(readOnly = true)
    	public Provider findAuditByRevision(Long id, int revision) {
    		AuditReader auditReader = AuditReaderFactory.get(em);
    		return auditReader.find(Provider.class, id, revision);
    	}
    }
    

    WarehouseServiceImpl.java

    package com.devblogs.service.impl;
    
    import java.util.List;
    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;
    import javax.persistence.TypedQuery;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.hibernate.envers.AuditReader;
    import org.hibernate.envers.AuditReaderFactory;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import com.devblogs.model.Warehouse;
    import com.devblogs.service.WarehouseService;
    
    @Service("warehouseService")
    @Transactional
    public class WarehouseServiceImpl implements WarehouseService {
    	private Log log = LogFactory.getLog(WarehouseServiceImpl.class);
    	
    	@PersistenceContext
    	private EntityManager em;
    
    	@Transactional(readOnly = true)
    	public List<Warehouse> findAll() {
    		return em.createQuery("from Warehouse w", Warehouse.class).getResultList();
    	}
    
    	@Transactional(readOnly = true)
    	public Warehouse findById(Long id) {
    		TypedQuery<Warehouse> typedQuery = em.createQuery("from Warehouse w where w.id = :id", Warehouse.class);
    		typedQuery.setParameter("id", id);
    		return typedQuery.getSingleResult();
    	}
    
    	@Transactional(readOnly = false)
    	public Warehouse save(Warehouse warehouse) {
    		if (warehouse.getId() == null) {
    			log.info("Inserting new warehouse");
    			em.persist(warehouse);
    		} else {
    			em.merge(warehouse);
    			log.info("Updating existing warehouse");
    		}
    		log.info("Warehouse saved with id: " + warehouse.getId());
    		return warehouse;
    	}
    
    	@Transactional(readOnly = false)
    	public void delete(Warehouse warehouse) {
    		Warehouse mergedWarehouse = em.merge(warehouse);
    		em.remove(mergedWarehouse);
    		log.info("Warehouse with id: " + warehouse.getId() + " deleted successfully");
    	}
    
    	@Transactional(readOnly = true)
    	public Warehouse findAuditByRevision(Long id, int revision) {
    		AuditReader auditReader = AuditReaderFactory.get(em);
    		return auditReader.find(Warehouse.class, id, revision);
    	}
    }
    

    Пакет com.devblogs.auditor

    Бин предоставляющий пользовательскую информацию аудита.

    AuditorAwareBean.java

    package com.devblogs.auditor;
    
    import org.springframework.data.domain.AuditorAware;
    
    public class AuditorAwareBean implements AuditorAware<String> {
    	public String getCurrentAuditor() {
    		return "username";
    	}
    }
    

    Конфигурация проекта

    Файл orm.xml мы создаём для того, чтобы указать в нём класс слушатель сущности JPA, который представляет службу аудита. Почему файл называется именно orm.xml и находится в каталоге META-INF потому что так определено в спецификации JPA. Класс который будет следить за сущностью JPA за событиями сохранения и обновления, называется org.springframework.data.jpa.domain.support.AuditingEntityListener. Еще нужно определить слушатель в спринговой конфигурации.

    META-INF/orm.xml

    <?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>
    

    А теперь добавим слушатели для хронологии. Чтобы при обновлении сущностного объекта его изменения записывались в таблицу хронологии, нужно за этим объектом следить с помощью модуля Hibernate Envers в виде слушателей EJB. Эти слушатели мы сконфигурируем в бине org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.
    Слушателем событий аудита Hibernate Envers является класс org.hibernate.envers.event.AuditEventListener. Он следит за каждым событием объекта и для каждого события (создания, обновления, удаления) он создает клон сущностного объекта и помещает его в таблицу хронологии. События, которые слушатель перехватывает, мы прописываем в свойствах бина org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean. В строках (42-62) в перечислены свойства с ключами событий и какие слушатели на них натравлены. Например:

    <prop key="hibernate.ejb.event.post-insert">
        org.hibernate.ejb.event.EJB3PostInsertEventListener,
        org.hibernate.envers.event.AuditEventListener
    </prop>
    

    означает, что слушатель org.hibernate.envers.event.AuditEventListener перехватывает событие после вставки.

    Дальше для Hibernate Envers идут свойства (строки 65-85). Эти свойства говорят Hibernate Envers как надо относиться к таблицам хронологии, которые мы добавили в базу данных, то есть как называются таблицы хронологии, как называются их поля и пр. Вот краткое описание:

    Свойство Описание
    org.hibernate.envers.audit_table_suffix Суффикс для имен таблицы хронологии, на базе имени исходной таблицы-сущности для которой отслеживается версия
    org.hibernate.envers.revision_field_name Столбец таблицы хронологии для сохранения текущего номера ревизии
    org.hibernate.envers.revision_type_field_name Столбец таблицы хронологии в котором сохраняется тип действия
    org.hibernate.envers.audit_strategy Стратегия аудита для отслеживания версий. Та о которой было описано в начале поста
    org.hibernate.envers.audit_strategy_validity_end_rev_field_name Столбец таблицы хронологии в котором сохраняется приемочная ревизия для текущего состояния
    org.hibernate.envers.audit_strategy_validity_store_revend_timestamp Время создания ревизии из текущего состояния в следующее. Означает сохранять это время или нет
    org.hibernate.envers.audit_strategy_validity_revend_timestamp_field_name Столбец таблицы хронологии в котором сохраняется время изменения из текущего состояния в следующее. Требуется только если предыдущее свойство установлено в true

    Если не можете выучить наизусть все свойства, забейте
    Полный файл спринговой конфигурации:

    spring/spring-data-app-context.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    	xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    	xmlns:tx="http://www.springframework.org/schema/tx"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
    		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
    		http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
    		http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd
    		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd">
    
        <bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        	<property name="driverClassName" value="com.mysql.jdbc.Driver" />
        	<property name="url" value="jdbc:mysql://localhost:3306/warehouse" />
        	<property name="username" value="username" />
            <property name="password" value="password" />
        </bean>
    
        <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
            <property name="entityManagerFactory" ref="emf"/>
        </bean>
    
        <tx:annotation-driven transaction-manager="transactionManager" />
    
        <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
            <property name="dataSource" ref="dataSource" />
            <property name="jpaVendorAdapter">
                <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
            </property>        
            <property name="packagesToScan" value="com.devblogs.model"/>
            <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>
                    
                    <!-- Listeners for Hibernate Envers -->              
                    <prop key="hibernate.ejb.event.post-insert">
                        org.hibernate.ejb.event.EJB3PostInsertEventListener,
                        org.hibernate.envers.event.AuditEventListener
                    </prop>   
                    <prop key="hibernate.ejb.event.post-update">
                        org.hibernate.ejb.event.EJB3PostUpdateEventListener,
                        org.hibernate.envers.event.AuditEventListener
                    </prop>
                    <prop key="hibernate.ejb.event.post-delete">
                        org.hibernate.ejb.event.EJB3PostDeleteEventListener,
                        org.hibernate.envers.event.AuditEventListener
                    </prop>
                    <prop key="hibernate.ejb.event.pre-collection-update">
                        org.hibernate.envers.event.AuditEventListener
                    </prop>
                    <prop key="hibernate.ejb.event.pre-collection-remove">
                        org.hibernate.envers.event.AuditEventListener
                    </prop>
                    <prop key="hibernate.ejb.event.post-collection-recreate">
                        org.hibernate.envers.event.AuditEventListener
                    </prop>
                    
                    <!-- Properties for Hibernate Envers --> 
                    <prop key="org.hibernate.envers.audit_table_suffix">
                        _chronology
                    </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>
    
        <context:annotation-config/>
    
        <context:component-scan base-package="com.devblogs"/>
        
        <jpa:auditing auditor-aware-ref="auditorAwareBean"/>
        
        <bean id="auditorAwareBean" class="com.devblogs.auditor.AuditorAwareBean"/>
    </beans>
    

    Дескриптор jpa:auditing включает средства аудита Spring Data JPA, а бин auditorAwareBean предоставляет пользовательские данные, кто именно изменил состояние объекта.

    log4j.properties

    log4j.rootCategory=DEBUG, stdout
    
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5d{ISO8601} %-5p [%15.15t] %-150l - %m%n
    
    log4j.category.org.apache.activemq=ERROR
    log4j.category.org.springframework.batch=DEBUG
    log4j.category.org.springframework.transaction=INFO
    
    log4j.category.org.hibernate.SQL=DEBUG
    # for debugging datasource initialization
    # log4j.category.test.jdbc=DEBUG
    

    pom.xml

    <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>com.devblogs</groupId>
    	<artifactId>simple-JPA-HibernateEnvers</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<packaging>jar</packaging>
    
    	<name>simpleHibernateEnvers</name>
    	<url>http://maven.apache.org</url>
    	
    	<properties>
    		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    		<spring.framework.version>3.1.0.RELEASE</spring.framework.version>
    	</properties>
    
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-test</artifactId>
    			<version>${spring.framework.version}</version>
    			<scope>test</scope>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-context-support</artifactId>
    			<version>${spring.framework.version}</version>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-orm</artifactId>
    			<version>${spring.framework.version}</version>
    		</dependency>
    		<dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
                <version>${spring.framework.version}</version>
            </dependency>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-aop</artifactId>
    			<version>${spring.framework.version}</version>
    		</dependency>
    		
    		<!-- Библиотека по отслеживанию версий сущностей -->
    		<dependency>
    			<groupId>org.hibernate</groupId>
    			<artifactId>hibernate-envers</artifactId>
    			<version>3.6.8.Final</version>
    		</dependency>
    		
    		<!-- Библиотека Spring Data JPA -->
    		<!-- Подтягивает org.springframework.data.domain.Auditable -->
    		<dependency>
                <groupId>org.springframework.data</groupId>
                <artifactId>spring-data-jpa</artifactId>
                <version>1.0.1.RELEASE</version>
            </dependency>
    		
    		<!-- База данных MySQL -->
    		<dependency>
        		<groupId>mysql</groupId>
        		<artifactId>mysql-connector-java</artifactId>
        		<version>5.1.39</version>
    		</dependency>
    		
    		<!-- Логирование -->
    		<dependency>
    			<groupId>commons-logging</groupId>
    			<artifactId>commons-logging</artifactId>
    			<version>1.1.1</version>
    		</dependency>
    		
            <!-- Библиотека для работы с датой и временем который используется Spring Data JPA -->
            <!-- Подтягивает org.joda.time.DateTime -->
    		<dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>2.0</version>
            </dependency>
            
            <!-- Эти библиотека нужна для библиотеки joda-time -->
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time-hibernate</artifactId>
                <version>1.3</version>
            </dependency>
            
    		<dependency>
    			<groupId>junit</groupId>
    			<artifactId>junit</artifactId>
    			<version>4.11</version>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    	
    	<build>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>1.2.1</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>java</goal>
                            </goals>
                        </execution>
                    </executions>   
                    <configuration>
                        <mainClass>com.devblogs.App</mainClass>
                    </configuration>             
                </plugin>
            </plugins>
        </build>
    </project>
    

    Мэин класс

    Тестировать будем регрессионно в мэйн методе, чтобы было наглядно что делается. Сначала создается новый объект поставщика, а затем через каждый 2 секунды происходит изменение состояния объекта поставщика. Все изменения документируются в таблице PROVIDERS_CHRONOLOGY, а в самом сущностном классе обновляется информация аудита. После выполнения метода мэйн так же нужно заглянуть в базу желательно до запуска метода мэин и после, чтобы посмотреть какие изменения произошли. В конце метода мы вытаскиваем сущностный объект из ревизий 1 и 2.

    App.java

    package com.devblogs;
    
    import org.springframework.context.support.GenericXmlApplicationContext;
    import com.devblogs.model.Provider;
    import com.devblogs.service.ProviderService;
    
    public class App {
    	public static void main(String[] args) throws Exception {
    		GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
    		ctx.load("classpath:spring/spring-data-app-context.xml");
    		ctx.refresh();
    		
    		ProviderService providerService = ctx.getBean("providerService", ProviderService.class);
    		
    		// Добавить новый провайдер
    		System.out.println("Добавить новый провайдер");
    		Provider provider = new Provider();
    		provider.setName("New Provider");
    		providerService.save(provider);
    		
    		Provider p = providerService.findById(provider.getId());
    		System.out.println(p);
    		
    		Thread.sleep(2000);
    		
    		// Обновить провайдер
    		System.out.println("Обновить провайдер");
    		provider.setName("Update provider");
    		providerService.save(provider);
    		p = providerService.findById(provider.getId());
    		System.out.println(p);
    		System.out.println();
    		
    		Thread.sleep(2000);
    		
    		// Обновить провайдер
    		System.out.println("Обновить провайдер еще раз");
    		provider.setName("test1");
    		providerService.save(provider);
    		p = providerService.findById(provider.getId());
    		System.out.println(p);
    		System.out.println();
    		
    		Thread.sleep(2000);
    		
    		// Обновить провайдер
    		System.out.println("Обновить провайдер еще раз");
    		provider.setName("test2");
    		providerService.save(provider);
    		p = providerService.findById(provider.getId());
    		System.out.println(p);
    		System.out.println();
    		
    		Thread.sleep(2000);
    		
    		// Найти запись аудита по номеру версии
    		System.out.println("Найти запись аудита по номеру версии");
    		Provider oldProvider = providerService.findAuditByRevision(6l, 1);
    		System.out.println();
    		System.out.println("Старый провайдер с id 1 и rev 1: " + oldProvider);
    		System.out.println();
    		oldProvider = providerService.findAuditByRevision(6l, 2);
    		System.out.println();
    		System.out.println("Старый провайдер с id 1 и rev 2: " + oldProvider);
    		System.out.println();
    	}
    }
    

    Запуск проекта

    Чтобы запустить проект можно выполнить команду mvn exec:java из командной строки:

    mvn exec:java
    SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
    SLF4J: Defaulting to no-operation (NOP) logger implementation
    SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
    Добавить новый провайдер
    Hibernate: insert into providers (id, created_by, created_date, last_modified_by, last_modified_date, name) values (null, ?, ?, ?, ?, ?)
    Hibernate: insert into REVINFO (REV, REVTSTMP) values (null, ?)
    Hibernate: insert into providers_chronology (action_type, audit_revision_end, audit_revision_end_ts, created_by, created_date, last_modified_by, last_modified_date, name, id, audit_revision) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    Hibernate: select distinct provider0_.id as id1_, provider0_.created_by as created2_1_, provider0_.created_date as created3_1_, provider0_.last_modified_by as last4_1_, provider0_.last_modified_date as last5_1_, provider0_.name as name1_ from providers provider0_ where provider0_.id=? limit ?
    Hibernate: select items0_.provider_id as provider1_1_2_, items0_.item_id as item2_2_, item1_.id as id0_0_, item1_.created_by as created2_0_0_, item1_.created_date as created3_0_0_, item1_.last_modified_by as last4_0_0_, item1_.last_modified_date as last5_0_0_, item1_.name as name0_0_, item1_.warehouse_id as warehouse7_0_0_, warehouse2_.id as id2_1_, warehouse2_.address as address2_1_, warehouse2_.created_by as created3_2_1_, warehouse2_.created_date as created4_2_1_, warehouse2_.last_modified_by as last5_2_1_, warehouse2_.last_modified_date as last6_2_1_ from items_providers items0_ inner join items item1_ on items0_.item_id=item1_.id left outer join warehouses warehouse2_ on item1_.warehouse_id=warehouse2_.id where items0_.provider_id=?
    
    [id: 7,
    name: New Provider,
    Created by: username,
    createDate: 2016-07-14T16:19:29.000+03:00,
    lastModifiedBy: username,
    lastModifiedDate: 2016-07-14T16:19:29.000+03:00]
    Обновить провайдер
    Hibernate: select provider0_.id as id1_1_, provider0_.created_by as created2_1_1_, provider0_.created_date as created3_1_1_, provider0_.last_modified_by as last4_1_1_, provider0_.last_modified_date as last5_1_1_, provider0_.name as name1_1_, items1_.provider_id as provider1_1_3_, item2_.id as item2_3_, item2_.id as id0_0_, item2_.created_by as created2_0_0_, item2_.created_date as created3_0_0_, item2_.last_modified_by as last4_0_0_, item2_.last_modified_date as last5_0_0_, item2_.name as name0_0_, item2_.warehouse_id as warehouse7_0_0_ from providers provider0_ left outer join items_providers items1_ on provider0_.id=items1_.provider_id left outer join items item2_ on items1_.item_id=item2_.id where provider0_.id=?
    Hibernate: update providers set created_by=?, created_date=?, last_modified_by=?, last_modified_date=?, name=? where id=?
    Hibernate: insert into REVINFO (REV, REVTSTMP) values (null, ?)
    Hibernate: select provider_c0_.id as id6_, provider_c0_.audit_revision as audit2_6_, provider_c0_.action_type as action3_6_, provider_c0_.audit_revision_end as audit4_6_, provider_c0_.audit_revision_end_ts as audit5_6_, provider_c0_.created_by as created6_6_, provider_c0_.created_date as created7_6_, provider_c0_.last_modified_by as last8_6_, provider_c0_.last_modified_date as last9_6_, provider_c0_.name as name6_ from providers_chronology provider_c0_ where provider_c0_.id=? and (provider_c0_.audit_revision_end is null) for update
    Hibernate: insert into providers_chronology (action_type, audit_revision_end, audit_revision_end_ts, created_by, created_date, last_modified_by, last_modified_date, name, id, audit_revision) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    Hibernate: update providers_chronology set audit_revision_end=?, audit_revision_end_ts=? where id=? and audit_revision=?
    Hibernate: select distinct provider0_.id as id1_, provider0_.created_by as created2_1_, provider0_.created_date as created3_1_, provider0_.last_modified_by as last4_1_, provider0_.last_modified_date as last5_1_, provider0_.name as name1_ from providers provider0_ where provider0_.id=? limit ?
    Hibernate: select items0_.provider_id as provider1_1_2_, items0_.item_id as item2_2_, item1_.id as id0_0_, item1_.created_by as created2_0_0_, item1_.created_date as created3_0_0_, item1_.last_modified_by as last4_0_0_, item1_.last_modified_date as last5_0_0_, item1_.name as name0_0_, item1_.warehouse_id as warehouse7_0_0_, warehouse2_.id as id2_1_, warehouse2_.address as address2_1_, warehouse2_.created_by as created3_2_1_, warehouse2_.created_date as created4_2_1_, warehouse2_.last_modified_by as last5_2_1_, warehouse2_.last_modified_date as last6_2_1_ from items_providers items0_ inner join items item1_ on items0_.item_id=item1_.id left outer join warehouses warehouse2_ on item1_.warehouse_id=warehouse2_.id where items0_.provider_id=?
    
    [id: 7,
    name: Update provider,
    Created by: username,
    createDate: 2016-07-14T16:19:29.000+03:00,
    lastModifiedBy: username,
    lastModifiedDate: 2016-07-14T16:19:31.000+03:00]
    
    Обновить провайдер еще раз
    Hibernate: select provider0_.id as id1_1_, provider0_.created_by as created2_1_1_, provider0_.created_date as created3_1_1_, provider0_.last_modified_by as last4_1_1_, provider0_.last_modified_date as last5_1_1_, provider0_.name as name1_1_, items1_.provider_id as provider1_1_3_, item2_.id as item2_3_, item2_.id as id0_0_, item2_.created_by as created2_0_0_, item2_.created_date as created3_0_0_, item2_.last_modified_by as last4_0_0_, item2_.last_modified_date as last5_0_0_, item2_.name as name0_0_, item2_.warehouse_id as warehouse7_0_0_ from providers provider0_ left outer join items_providers items1_ on provider0_.id=items1_.provider_id left outer join items item2_ on items1_.item_id=item2_.id where provider0_.id=?
    Hibernate: update providers set created_by=?, created_date=?, last_modified_by=?, last_modified_date=?, name=? where id=?
    Hibernate: insert into REVINFO (REV, REVTSTMP) values (null, ?)
    Hibernate: select provider_c0_.id as id6_, provider_c0_.audit_revision as audit2_6_, provider_c0_.action_type as action3_6_, provider_c0_.audit_revision_end as audit4_6_, provider_c0_.audit_revision_end_ts as audit5_6_, provider_c0_.created_by as created6_6_, provider_c0_.created_date as created7_6_, provider_c0_.last_modified_by as last8_6_, provider_c0_.last_modified_date as last9_6_, provider_c0_.name as name6_ from providers_chronology provider_c0_ where provider_c0_.id=? and (provider_c0_.audit_revision_end is null) for update
    Hibernate: insert into providers_chronology (action_type, audit_revision_end, audit_revision_end_ts, created_by, created_date, last_modified_by, last_modified_date, name, id, audit_revision) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    Hibernate: update providers_chronology set audit_revision_end=?, audit_revision_end_ts=? where id=? and audit_revision=?
    Hibernate: select distinct provider0_.id as id1_, provider0_.created_by as created2_1_, provider0_.created_date as created3_1_, provider0_.last_modified_by as last4_1_, provider0_.last_modified_date as last5_1_, provider0_.name as name1_ from providers provider0_ where provider0_.id=? limit ?
    Hibernate: select items0_.provider_id as provider1_1_2_, items0_.item_id as item2_2_, item1_.id as id0_0_, item1_.created_by as created2_0_0_, item1_.created_date as created3_0_0_, item1_.last_modified_by as last4_0_0_, item1_.last_modified_date as last5_0_0_, item1_.name as name0_0_, item1_.warehouse_id as warehouse7_0_0_, warehouse2_.id as id2_1_, warehouse2_.address as address2_1_, warehouse2_.created_by as created3_2_1_, warehouse2_.created_date as created4_2_1_, warehouse2_.last_modified_by as last5_2_1_, warehouse2_.last_modified_date as last6_2_1_ from items_providers items0_ inner join items item1_ on items0_.item_id=item1_.id left outer join warehouses warehouse2_ on item1_.warehouse_id=warehouse2_.id where items0_.provider_id=?
    
    [id: 7,
    name: test1,
    Created by: username,
    createDate: 2016-07-14T16:19:29.000+03:00,
    lastModifiedBy: username,
    lastModifiedDate: 2016-07-14T16:19:33.000+03:00]
    
    Обновить провайдер еще раз
    Hibernate: select provider0_.id as id1_1_, provider0_.created_by as created2_1_1_, provider0_.created_date as created3_1_1_, provider0_.last_modified_by as last4_1_1_, provider0_.last_modified_date as last5_1_1_, provider0_.name as name1_1_, items1_.provider_id as provider1_1_3_, item2_.id as item2_3_, item2_.id as id0_0_, item2_.created_by as created2_0_0_, item2_.created_date as created3_0_0_, item2_.last_modified_by as last4_0_0_, item2_.last_modified_date as last5_0_0_, item2_.name as name0_0_, item2_.warehouse_id as warehouse7_0_0_ from providers provider0_ left outer join items_providers items1_ on provider0_.id=items1_.provider_id left outer join items item2_ on items1_.item_id=item2_.id where provider0_.id=?
    Hibernate: update providers set created_by=?, created_date=?, last_modified_by=?, last_modified_date=?, name=? where id=?
    Hibernate: insert into REVINFO (REV, REVTSTMP) values (null, ?)
    Hibernate: select provider_c0_.id as id6_, provider_c0_.audit_revision as audit2_6_, provider_c0_.action_type as action3_6_, provider_c0_.audit_revision_end as audit4_6_, provider_c0_.audit_revision_end_ts as audit5_6_, provider_c0_.created_by as created6_6_, provider_c0_.created_date as created7_6_, provider_c0_.last_modified_by as last8_6_, provider_c0_.last_modified_date as last9_6_, provider_c0_.name as name6_ from providers_chronology provider_c0_ where provider_c0_.id=? and (provider_c0_.audit_revision_end is null) for update
    Hibernate: insert into providers_chronology (action_type, audit_revision_end, audit_revision_end_ts, created_by, created_date, last_modified_by, last_modified_date, name, id, audit_revision) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    Hibernate: update providers_chronology set audit_revision_end=?, audit_revision_end_ts=? where id=? and audit_revision=?
    Hibernate: select distinct provider0_.id as id1_, provider0_.created_by as created2_1_, provider0_.created_date as created3_1_, provider0_.last_modified_by as last4_1_, provider0_.last_modified_date as last5_1_, provider0_.name as name1_ from providers provider0_ where provider0_.id=? limit ?
    Hibernate: select items0_.provider_id as provider1_1_2_, items0_.item_id as item2_2_, item1_.id as id0_0_, item1_.created_by as created2_0_0_, item1_.created_date as created3_0_0_, item1_.last_modified_by as last4_0_0_, item1_.last_modified_date as last5_0_0_, item1_.name as name0_0_, item1_.warehouse_id as warehouse7_0_0_, warehouse2_.id as id2_1_, warehouse2_.address as address2_1_, warehouse2_.created_by as created3_2_1_, warehouse2_.created_date as created4_2_1_, warehouse2_.last_modified_by as last5_2_1_, warehouse2_.last_modified_date as last6_2_1_ from items_providers items0_ inner join items item1_ on items0_.item_id=item1_.id left outer join warehouses warehouse2_ on item1_.warehouse_id=warehouse2_.id where items0_.provider_id=?
    
    [id: 7,
    name: test2,
    Created by: username,
    createDate: 2016-07-14T16:19:29.000+03:00,
    lastModifiedBy: username,
    lastModifiedDate: 2016-07-14T16:19:35.000+03:00]
    
    Найти запись аудита по номеру версии
    Hibernate: select provider_c0_.id as id6_, provider_c0_.audit_revision as audit2_6_, provider_c0_.action_type as action3_6_, provider_c0_.audit_revision_end as audit4_6_, provider_c0_.audit_revision_end_ts as audit5_6_, provider_c0_.created_by as created6_6_, provider_c0_.created_date as created7_6_, provider_c0_.last_modified_by as last8_6_, provider_c0_.last_modified_date as last9_6_, provider_c0_.name as name6_ from providers_chronology provider_c0_ where provider_c0_.audit_revision<=? and provider_c0_.action_type<>? and provider_c0_.id=? and (provider_c0_.audit_revision_end>? or provider_c0_.audit_revision_end is null)
    
    Старый провайдер с id 1 и rev 1: 
    [id: 6,
    name: New Provider,
    Created by: username,
    createDate: 2016-07-13T19:18:01.000+03:00,
    lastModifiedBy: username,
    lastModifiedDate: 2016-07-13T19:17:59.000+03:00]
    
    Hibernate: select provider_c0_.id as id6_, provider_c0_.audit_revision as audit2_6_, provider_c0_.action_type as action3_6_, provider_c0_.audit_revision_end as audit4_6_, provider_c0_.audit_revision_end_ts as audit5_6_, provider_c0_.created_by as created6_6_, provider_c0_.created_date as created7_6_, provider_c0_.last_modified_by as last8_6_, provider_c0_.last_modified_date as last9_6_, provider_c0_.name as name6_ from providers_chronology provider_c0_ where provider_c0_.audit_revision<=? and provider_c0_.action_type<>? and provider_c0_.id=? and (provider_c0_.audit_revision_end>? or provider_c0_.audit_revision_end is null)
    
    Старый провайдер с id 1 и rev 2: 
    [id: 6,
    name: Update provider,
    Created by: username,
    createDate: 2016-07-13T19:18:03.000+03:00,
    lastModifiedBy: username,
    lastModifiedDate: 2016-07-13T19:18:01.000+03:00]
    

    Поделиться в социальных сетях

    Опубликовать в Google Plus
    Опубликовать в LiveJournal
    Опубликовать в Мой Мир
    Опубликовать в Одноклассники
    Опубликовать в Яндекс

    Добавить комментарий

    Ваш e-mail не будет опубликован. Обязательные поля помечены *