Перейти к основному содержимому

Использование JaVers для аудита модели данных в Spring Data

· 10 мин. чтения

1. Обзор

В этом руководстве мы увидим, как настроить и использовать JaVers в простом приложении Spring Boot для отслеживания изменений сущностей.

2. ЯВерс

При работе с изменяемыми данными у нас обычно есть только последнее состояние сущности, хранящееся в базе данных. Как разработчики, мы тратим много времени на отладку приложения, поиск в лог-файлах события, изменившего состояние. Это становится еще сложнее в производственной среде, когда систему использует множество разных пользователей.

К счастью, у нас есть отличные инструменты, такие как 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 . В этом руководстве мы будем использовать стартер Spring Boot SQL.

<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-spring-boot-starter-sql</artifactId>
<version>6.5.3</version>
</dependency>

Поскольку мы собираемся использовать базу данных H2, давайте также включим эту зависимость:

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>

3.2. Настройка репозитория JaVers

JaVers использует абстракцию репозитория для хранения коммитов и сериализованных сущностей. Все данные хранятся в формате JSON . Поэтому, возможно, лучше использовать хранилище NoSQL. Однако для простоты мы будем использовать инстанс H2 в памяти .

По умолчанию JaVers использует реализацию репозитория в памяти, и если мы используем Spring Boot, дополнительная настройка не требуется. Кроме того, при использовании стартеров Spring Data JaVers повторно использует конфигурацию базы данных для приложения .

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

3.3. ЯВерс Недвижимость

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

Давайте переопределим только один, newObjectSnapshot , чтобы мы могли получать моментальные снимки только что созданных объектов:

javers.newObjectSnapshot=true

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

JaVers внутренне определяет следующие типы: Entities, Value Objects, Values, Containers и Primitives. Некоторые из этих терминов взяты из терминологии 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<Product> 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<Store, Integer> {
}

Кроме того, у нас будет ProductRepository , но без аннотаций:

public interface ProductRepository extends CrudRepository<Product, Integer> {
}

Теперь рассмотрим случай, когда мы не используем репозитории Spring Data. Для этой цели в JaVers есть еще одна аннотация уровня метода: @JaversAuditable.

Например, мы можем определить метод сохранения продукта следующим образом:

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

Кроме того, мы можем даже добавить эту аннотацию прямо над методом в интерфейсе репозитория:

public interface ProductRepository extends CrudRepository<Product, Integer> {
@Override
@JaversAuditable
<S extends Product> S save(S s);
}

4.3. Автор Поставщик

Каждое зафиксированное изменение в JaVers должно иметь своего автора. Более того, JaVers поддерживает Spring Security из коробки.

В результате каждая фиксация выполняется конкретным аутентифицированным пользователем. Однако для этого урока мы создадим действительно простую пользовательскую реализацию интерфейса AuthorProvider :

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

И в качестве последнего шага, чтобы заставить JaVers использовать нашу пользовательскую реализацию, нам нужно переопределить bean-компонент конфигурации по умолчанию:

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

5. Аудит ЯВерс

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

./82cbf8bbd2e5c205ab6d1d03df0e11e5.png

Чтобы получить некоторые начальные образцы данных, давайте воспользуемся EventListener для заполнения нашей базы данных некоторыми продуктами:

@EventListener
public void appReady(ApplicationReadyEvent event) {
Store store = new Store("ForEach 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. Начальная фиксация

Когда объект создается, JaVers сначала делает коммит типа INITIAL .

Проверим снимки после запуска приложения:

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

В приведенном выше коде мы запрашиваем у JaVers моментальные снимки для класса Store . Если мы сделаем запрос к этой конечной точке, мы получим результат, подобный приведенному ниже:

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

Обратите внимание, что приведенный выше снимок включает все продукты, добавленные в магазин, несмотря на отсутствие аннотации для интерфейса ProductRepository .

По умолчанию JaVers проверяет все связанные модели совокупного корня, если они сохраняются вместе с родителем.

Мы можем указать JaVers игнорировать определенные классы, используя аннотацию DiffIgnore .

Например, мы можем аннотировать поле products аннотацией в сущности Store :

@DiffIgnore
private List<Product> products = new ArrayList<>();

Следовательно, JaVers не будет отслеживать изменения продуктов, происходящих из объекта Магазина .

5.2. Обновить фиксацию

Следующий тип коммита — это коммит UPDATE . Это наиболее ценный тип фиксации, поскольку он представляет изменения состояния объекта.

Давайте определим метод, который будет обновлять сущность магазина и все товары в магазине:

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

Если мы запустим этот метод, мы получим следующую строку в выводе отладки (в случае того же количества продуктов и магазинов):

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

Поскольку JaVers успешно сохранил изменения, давайте запросим снимки для продуктов:

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

Мы получим предыдущие коммиты INITIAL и новые коммиты UPDATE :

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

Здесь мы можем увидеть всю информацию о внесенных нами изменениях.

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

5.3. Изменения

JaVers записывает изменения как атомарные различия между версиями объекта . Как видно из схемы JaVers, отдельной таблицы для хранения изменений нет, поэтому JaVers вычисляет изменения динамически как разницу между снапшотами .

Обновим цену товара:

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

Затем давайте запросим JaVers об изменениях:

@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);
}

Вывод содержит измененное свойство и его значения до и после:

[
{
"changeType": "ValueChange",
"globalId": {
"entity": "com.foreach.springjavers.domain.Product",
"cdoId": 2
},
"commitMetadata": {
"author": "ForEach 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
}
]

Чтобы определить тип изменения, JaVers сравнивает последующие снимки обновлений объекта. В приведенном выше случае, когда мы изменили свойство объекта, мы получили тип изменения PROPERTY_VALUE_CHANGED .

5.4. Тени

Более того, JaVers предоставляет еще один вид проверяемых сущностей, который называется Shadow . Тень представляет собой состояние объекта, восстановленное из моментальных снимков. Эта концепция тесно связана с Event Sourcing .

Существует четыре различных области применения теней:

  • Неглубоко — тени создаются из моментального снимка, выбранного в запросе JQL.
  • Дочерний объект-значение — тени содержат все дочерние объекты-значения, принадлежащие выбранным сущностям.
  • Commit-deep — тени создаются из всех снимков, связанных с выбранными сущностями.
  • Deep+ — JaVers пытается восстановить полные графы объектов с (возможно) всеми загруженными объектами.

Давайте воспользуемся областью объекта Child-value и получим тень для одного хранилища:

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

В результате мы получим сущность магазина с объектом Address value:

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

Чтобы получить продукты в результате, мы можем применить Commit-deep scope.

6. Заключение

В этом руководстве мы увидели, как легко JaVers интегрируется, в частности, со Spring Boot и Spring Data. В целом, для установки JaVers практически не требуется никаких настроек.

В заключение, у JaVers могут быть разные приложения, от отладки до сложного анализа.

Полный проект для этой статьи доступен на GitHub .