Синопсис
Аудит и отслеживание версий бизнес данных применяется в более или менее серьёзных корпоративных приложениях. Так как такие сущности как поставщик, склад и продукция являются основными бизнес объектами манипулирования в нашем приложении, то может возникнуть потребность за ними наблюдать, другими словами может потребоваться иметь информацию о том кто изменил состояние объекта (это называется аудит) а также какое состояние этот объект имел до этого изменения или какое состояние у этого объекта было несколько изменений назад (это называется версионированием), то есть мы хотим держать всю систему под колпаком. В этом посте мы рассмотрим два этих аспекта (аудит и версионирование сущностных классов) на пример нашего приложения по работе со складской базой данных Warehouse.
Похожие посты
Весь проект можно взять с гитхаба: 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]