Використання JaVers для аудиту моделі даних у Spring Data

1. Огляд

У цьому підручнику ми побачимо, як налаштувати та використовувати JaVers у простому додатку Spring Boot для відстеження змін сутностей.

2. JaVers

При роботі з змінними даними ми зазвичай маємо лише останній стан сутності, що зберігається в базі даних. Як розробники, ми витрачаємо багато часу на налагодження програми, шукаючи у файлах журналів подію, яка змінила стан. Це стає ще складнішим у виробничому середовищі, коли багато різних користувачів використовують систему.

На щастя, у нас є чудові інструменти, такі як JaVers. JaVers - це структура журналу аудиту, яка допомагає відстежувати зміни сутностей у програмі.

Використання цього інструменту не обмежується лише налагодженням та аудитом. Його можна успішно застосувати для проведення аналізу, примусової політики безпеки та ведення журналу подій.

3. Налаштування проекту

Перш за все, щоб почати використовувати JaVers, нам потрібно налаштувати сховище аудиту для постійних знімків сутностей. По-друге, нам потрібно налаштувати деякі настроювані властивості JaVers. Нарешті, ми також розглянемо, як правильно налаштувати наші моделі доменів.

Але варто зазначити, що JaVers надає параметри конфігурації за замовчуванням, тому ми можемо почати використовувати його майже без конфігурації.

3.1. Залежності

По-перше, нам потрібно додати до нашого проекту початкову залежність JaVers Spring Boot. Залежно від типу накопичувача, ми маємо два варіанти: org.javers: javers-spring-boot-starter-sql та org.javers: javers-spring-boot-starter-mongo . У цьому навчальному посібнику ми будемо використовувати SQL Boter Spring Boot.

 org.javers javers-spring-boot-starter-sql 5.6.3 

Оскільки ми будемо використовувати базу даних H2, давайте також включимо цю залежність:

 com.h2database h2 

3.2. Налаштування репозиторію JaVers

JaVers використовує абстракцію сховища для зберігання комітів та серіалізованих сутностей. Усі дані зберігаються у форматі JSON. Отже, цілком підходить використання сховища NoSQL. Однак для простоти ми будемо використовувати екземпляр H2 в пам'яті.

За замовчуванням JaVers використовує реалізацію сховища в пам'яті, і якщо ми використовуємо Spring Boot, немає потреби в додатковій конфігурації. Крім того, під час використання Spring Data starters, JaVers повторно використовує конфігурацію бази даних для програми .

JaVers надає два пускачі для стеків стійкості SQL та Mongo. Вони сумісні з Spring Data і за замовчуванням не потребують додаткової конфігурації. Однак ми завжди можемо замінити конфігураційні компоненти за замовчуванням: JaversSqlAutoConfiguration.java та JaversMongoAutoConfiguration.java відповідно.

3.3. Властивості JaVers

JaVers дозволяє налаштувати декілька параметрів, хоча за замовчуванням Spring Boot у більшості випадків достатньо.

Давайте перевизначимо лише один, newObjectSnapshot , щоб ми могли отримати знімки новостворених об'єктів:

javers.newObjectSnapshot=true 

3.4. Конфігурація домену JaVers

JaVers внутрішньо визначає такі типи: сутності, об'єкти вартості, значення, контейнери та примітиви. Деякі з цих термінів походять від термінології DDD (Domain Driven Design).

Основною метою наявності декількох типів є надання різних алгоритмів різниці залежно від типу . Кожен тип має відповідну стратегію різниці. Як наслідок, якщо класи програм налаштовані неправильно, ми отримаємо непередбачувані результати.

Щоб сказати JaVers, який тип використовувати для класу, у нас є кілька варіантів:

  • Явно - перший варіант - явне використання методів register * класу JaversBuilder - другий спосіб - використання анотацій
  • Неявно - JaVers надає алгоритми для автоматичного виявлення типів на основі відносин класу
  • За замовчуванням - за замовчуванням JaVers буде розглядати всі класи як ValueObjects

У цьому посібнику ми налаштуємо JaVers явно, використовуючи метод анотації.

Чудовим є те, що JaVers сумісний з анотаціями javax.persistence . Як результат, нам не потрібно буде використовувати спеціальні анотації JaVers для наших сутностей.

4. Зразок проекту

Зараз ми збираємося створити просту програму, яка включатиме кілька сутностей домену, які ми будемо перевіряти.

4.1. Моделі доменів

Наш домен включатиме магазини з продуктами.

Давайте визначимо сутність Store :

@Entity public class Store { @Id @GeneratedValue private int id; private String name; @Embedded private Address address; @OneToMany( mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true ) private List products = new ArrayList(); // constructors, getters, setters }

Зверніть увагу, що ми використовуємо анотації JPA за замовчуванням. JaVers відображає їх таким чином:

  • @ javax.persistence.Entity зіставляється з @ org.javers.core.metamodel.annotation.Entity
  • @ javax.persistence.Embeddable відображається на @ org.javers.core.metamodel.annotation.ValueObject.

Класи, які можна вбудовувати, визначаються звичайним чином:

@Embeddable public class Address { private String address; private Integer zipCode; }

4.2. Сховища даних

Для аудиту сховищ JPA JaVers надає анотацію @JaversSpringDataAuditable .

Давайте визначимо StoreRepository з цією анотацією:

@JaversSpringDataAuditable public interface StoreRepository extends CrudRepository { }

Крім того, у нас буде ProductRepository , але без приміток:

public interface ProductRepository extends CrudRepository { }

Тепер розглянемо випадок, коли ми не використовуємо сховища Spring Data. Для цього JaVers має ще одну анотацію рівня методу: @JaversAuditable.

Наприклад, ми можемо визначити спосіб збереження товару таким чином:

@JaversAuditable public void saveProduct(Product product) { // save object }

Крім того, ми можемо навіть додати цю анотацію безпосередньо над методом в інтерфейсі сховища:

public interface ProductRepository extends CrudRepository { @Override @JaversAuditable  S save(S s); }

4.3. Автор Постачальник

Кожна здійснена зміна в JaVers повинна мати свого автора. Більше того, JaVers підтримує Spring Security нестандартно.

Як результат, кожне комітування здійснюється певним аутентифікованим користувачем. Однак для цього підручника ми створимо дуже просту користувацьку реалізацію інтерфейсу AuthorProvider :

private static class SimpleAuthorProvider implements AuthorProvider { @Override public String provide() { return "Baeldung Author"; } }

І як останній крок, щоб змусити JaVers використовувати нашу спеціальну реалізацію, нам потрібно замінити компонент конфігурації за замовчуванням:

@Bean public AuthorProvider provideJaversAuthor() { return new SimpleAuthorProvider(); }

5. Аудит JaVers

Нарешті, ми готові перевірити нашу заявку. Ми будемо використовувати простий контролер для розсилки змін у нашому додатку та отримання журналу комітетів JaVers. Крім того, ми також можемо отримати доступ до консолі H2, щоб побачити внутрішню структуру нашої бази даних:

Щоб мати деякі початкові зразки даних, давайте використаємо EventListener, щоб заповнити базу даних деякими продуктами:

@EventListener public void appReady(ApplicationReadyEvent event) { Store store = new Store("Baeldung store", new Address("Some street", 22222)); for (int i = 1; i < 3; i++) { Product product = new Product("Product #" + i, 100 * i); store.addProduct(product); } storeRepository.save(store); }

5.1. Початкове комітування

When an object is created, JaVers first makes a commit of the INITIAL type.

Let’s check the snapshots after the application startup:

@GetMapping("/stores/snapshots") public String getStoresSnapshots() { QueryBuilder jqlQuery = QueryBuilder.byClass(Store.class); List snapshots = javers.findSnapshots(jqlQuery.build()); return javers.getJsonConverter().toJson(snapshots); }

In the code above, we're querying JaVers for snapshots for the Store class. If we make a request to this endpoint we’ll get a result like the one below:

[ { "commitMetadata": { "author": "Baeldung Author", "properties": [], "commitDate": "2019-08-26T07:04:06.776", "commitDateInstant": "2019-08-26T04:04:06.776Z", "id": 1.00 }, "globalId": { "entity": "com.baeldung.springjavers.domain.Store", "cdoId": 1 }, "state": { "address": { "valueObject": "com.baeldung.springjavers.domain.Address", "ownerId": { "entity": "com.baeldung.springjavers.domain.Store", "cdoId": 1 }, "fragment": "address" }, "name": "Baeldung store", "id": 1, "products": [ { "entity": "com.baeldung.springjavers.domain.Product", "cdoId": 2 }, { "entity": "com.baeldung.springjavers.domain.Product", "cdoId": 3 } ] }, "changedProperties": [ "address", "name", "id", "products" ], "type": "INITIAL", "version": 1 } ]

Note that the snapshot above includes all products added to the store despite the missing annotation for the ProductRepository interface.

By default, JaVers will audit all related models of an aggregate root if they are persisted along with the parent.

We can tell JaVers to ignore specific classes by using the DiffIgnore annotation.

For instance, we may annotate the products field with the annotation in the Store entity:

@DiffIgnore private List products = new ArrayList();

Consequently, JaVers won’t track changes of products originated from the Store entity.

5.2. Update Commit

The next type of commit is the UPDATE commit. This is the most valuable commit type as it represents changes of an object's state.

Let’s define a method that will update the store entity and all products in the store:

public void rebrandStore(int storeId, String updatedName) { Optional storeOpt = storeRepository.findById(storeId); storeOpt.ifPresent(store -> { store.setName(updatedName); store.getProducts().forEach(product -> { product.setNamePrefix(updatedName); }); storeRepository.save(store); }); }

If we run this method we'll get the following line in the debug output (in case of the same products and stores count):

11:29:35.439 [http-nio-8080-exec-2] INFO org.javers.core.Javers - Commit(id:2.0, snapshots:3, author:Baeldung Author, changes - ValueChange:3), done in 48 millis (diff:43, persist:5)

Since JaVers has persisted changes successfully, let’s query the snapshots for products:

@GetMapping("/products/snapshots") public String getProductSnapshots() { QueryBuilder jqlQuery = QueryBuilder.byClass(Product.class); List snapshots = javers.findSnapshots(jqlQuery.build()); return javers.getJsonConverter().toJson(snapshots); }

We'll get previous INITIAL commits and new UPDATE commits:

 { "commitMetadata": { "author": "Baeldung Author", "properties": [], "commitDate": "2019-08-26T12:55:20.197", "commitDateInstant": "2019-08-26T09:55:20.197Z", "id": 2.00 }, "globalId": { "entity": "com.baeldung.springjavers.domain.Product", "cdoId": 3 }, "state": { "price": 200.0, "name": "NewProduct #2", "id": 3, "store": { "entity": "com.baeldung.springjavers.domain.Store", "cdoId": 1 } } }

Here, we can see all the information about the change we made.

It is worth noting that JaVers doesn’t create new connections to the database. Instead, it reuses existing connections. JaVers data is committed or rolled back along with application data in the same transaction.

5.3. Changes

JaVers records changes as atomic differences between versions of an object. As we may see from the JaVers scheme, there is no separate table for storing changes, so JaVers calculates changes dynamically as the difference between snapshots.

Let’s update a product price:

public void updateProductPrice(Integer productId, Double price) { Optional productOpt = productRepository.findById(productId); productOpt.ifPresent(product -> { product.setPrice(price); productRepository.save(product); }); }

Then, let's query JaVers for changes:

@GetMapping("/products/{productId}/changes") public String getProductChanges(@PathVariable int productId) { Product product = storeService.findProductById(productId); QueryBuilder jqlQuery = QueryBuilder.byInstance(product); Changes changes = javers.findChanges(jqlQuery.build()); return javers.getJsonConverter().toJson(changes); }

The output contains the changed property and its values before and after:

[ { "changeType": "ValueChange", "globalId": { "entity": "com.baeldung.springjavers.domain.Product", "cdoId": 2 }, "commitMetadata": { "author": "Baeldung Author", "properties": [], "commitDate": "2019-08-26T16:22:33.339", "commitDateInstant": "2019-08-26T13:22:33.339Z", "id": 2.00 }, "property": "price", "propertyChangeType": "PROPERTY_VALUE_CHANGED", "left": 100.0, "right": 3333.0 } ]

To detect a type of a change JaVers compares subsequent snapshots of an object's updates. In the case above as we've changed the property of the entity we've got the PROPERTY_VALUE_CHANGED change type.

5.4. Shadows

Moreover, JaVers provides another view of audited entities called Shadow. A Shadow represents an object state restored from snapshots. This concept is closely related to Event Sourcing.

There are four different scopes for Shadows:

  • Shallow — shadows are created from a snapshot selected within a JQL query
  • Child-value-object — shadows contain all child value objects owned by selected entities
  • Заглиблення коміту - тіні створюються з усіх знімків, пов’язаних із вибраними сутностями
  • Глибоко + - JaVers намагається відновити повні графіки об'єктів за допомогою (можливо) всіх завантажених об'єктів.

Давайте скористаємося об’єктом Child-value-object і отримаємо тінь для одного магазину:

@GetMapping("/stores/{storeId}/shadows") public String getStoreShadows(@PathVariable int storeId) { Store store = storeService.findStoreById(storeId); JqlQuery jqlQuery = QueryBuilder.byInstance(store) .withChildValueObjects().build(); List
    
      shadows = javers.findShadows(jqlQuery); return javers.getJsonConverter().toJson(shadows.get(0)); }
    

В результаті ми отримаємо сутність магазину з об’єктом значення Address :

{ "commitMetadata": { "author": "Baeldung Author", "properties": [], "commitDate": "2019-08-26T16:09:20.674", "commitDateInstant": "2019-08-26T13:09:20.674Z", "id": 1.00 }, "it": { "id": 1, "name": "Baeldung store", "address": { "address": "Some street", "zipCode": 22222 }, "products": [] } }

Щоб отримати результати в результаті, ми можемо застосувати обширний коміт.

6. Висновок

У цьому посібнику ми побачили, наскільки легко JaVers інтегрується з Spring Boot та Spring Data, зокрема. Загалом, для налаштування JaVers потрібна майже нульова конфігурація.

На закінчення, JaVers можуть мати різні програми, від налагодження до складного аналізу.

Повний проект цієї статті доступний на GitHub.