Using JaVers for Data Model Auditing in Spring Data – 在Spring Data中使用JaVers进行数据模型审计

最后修改: 2019年 9月 29日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll see how to set up and use JaVers in a simple Spring Boot application to track changes of entities.

在本教程中,我们将看到如何在一个简单的Spring Boot应用程序中设置和使用JaVers来跟踪实体的变化。

2. JaVers

2.jaVers

When dealing with mutable data we usually have only the last state of an entity stored in a database. As developers, we spend a lot of time debugging an application, searching through log files for an event that changed a state. This gets even trickier in the production environment when lots of different users are using the system.

在处理易变数据时,我们通常只有存储在数据库中的一个实体的最后状态。作为开发者,我们花了很多时间来调试一个应用程序,在日志文件中搜索改变状态的事件。在生产环境中,当很多不同的用户在使用这个系统时,这就变得更加棘手了。

Fortunately, we have great tools like JaVers. JaVers is an audit log framework that helps to track changes of entities in the application.

幸运的是,我们有像JaVers这样的好工具。JaVers是一个审计日志框架,有助于跟踪应用程序中实体的变化。

The usage of this tool is not limited to debugging and auditing only. It can be successfully applied to perform analysis, force security policies and maintaining the event log, too.

这个工具的用途不仅限于调试和审计。它也可以成功地应用于执行分析、强制安全策略和维护事件日志。

3. Project Set-up

3.项目设置</b

First of all, to start using JaVers we need to configure the audit repository for persisting snapshots of entities. Secondly, we need to adjust some configurable properties of JaVers. Finally, we’ll also cover how to configure our domain models properly.

首先,为了开始使用JaVers,我们需要配置审计库以持久化实体的快照。其次,我们需要调整JaVers的一些可配置属性。最后,我们还将介绍如何正确配置我们的领域模型。

But, it worth mentioning that JaVers provides default configuration options, so we can start using it with almost no configuration.

但是,值得一提的是,JaVers提供了默认的配置选项,所以我们几乎不需要配置就可以开始使用它。

3.1. Dependencies

3.1. 依赖性

First, we need to add the JaVers Spring Boot starter dependency to our project. Depending on the type of persistence storage, we have two options: org.javers:javers-spring-boot-starter-sql and org.javers:javers-spring-boot-starter-mongo. In this tutorial, we’ll use the Spring Boot SQL starter.

首先,我们需要将JaVers Spring Boot启动器的依赖性添加到我们的项目中。根据持久性存储的类型,我们有两个选择。org.javers:javers-spring-boot-start-sqlorg.javers:javers-spring-boot-start-mongo。在本教程中,我们将使用Spring Boot的SQL启动器。

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

As we are going to use the H2 database, let’s also include this dependency:

由于我们要使用H2数据库,我们也要包括这个依赖关系。

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

3.2. JaVers Repository Setup

3.2.JaVers 仓库的设置</b

JaVers uses a repository abstraction for storing commits and serialized entities. All data is stored in the JSON format. Therefore, it might be a good fit to use a NoSQL storage. However, for the sake of simplicity, we’ll use an H2 in-memory instance.

JaVers使用一个存储库抽象来存储提交和序列化的实体。所有数据都以JSON格式存储。因此,使用NoSQL存储可能是很合适的。然而,为了简单起见,我们将使用一个H2内存实例

By default, JaVers leverages an in-memory repository implementation, and if we’re using Spring Boot, there is no need for extra configuration. Furthermore, while using Spring Data starters, JaVers reuses the database configuration for the application.

默认情况下,JaVers利用内存中的存储库实现,如果我们使用Spring Boot,就不需要进行额外的配置。此外,在使用Spring Data启动器时,JaVers会重复使用应用程序的数据库配置

JaVers provides two starters for SQL and Mongo persistence stacks.  They are compatible with Spring Data and don’t require extra configuration by default. However, we can always override default configuration beans: JaversSqlAutoConfiguration.java and JaversMongoAutoConfiguration.java respectively.

JaVers为SQL和Mongo持久化堆栈提供了两个启动器。 它们与Spring Data兼容,默认情况下不需要额外的配置。然而,我们总是可以覆盖默认的配置Bean。JaversSqlAutoConfiguration.javaJaversMongoAutoConfiguration.java分别。

3.3. JaVers Properties

3.3 JaVers属性

JaVers allows configuring several options, though the Spring Boot defaults are sufficient in most use cases.

JaVers允许配置几个选项,尽管Spring Boot的默认值在大多数使用情况下已经足够。

Let’s override just one, newObjectSnapshot, so that we can get snapshots of newly created objects:

让我们只覆盖一个,newObjectSnapshot,这样我们就可以获得新创建对象的快照。

javers.newObjectSnapshot=true

3.4. JaVers Domain Configuration

3.4.JaVers域配置

JaVers internally defines the following types: Entities, Value Objects, Values, Containers, and Primitives. Some of these terms come from DDD (Domain Driven Design) terminology.

JaVers内部定义了以下类型。实体、价值对象、价值、容器和基元。其中一些术语来自DDD(Domain Driven Design)术语。

The main purpose of having several types is to provide different diff algorithms depending on the type. Each type has a corresponding diff strategy. As a consequence, if application classes are configured incorrectly we’ll get unpredictable results.

有几种类型的主要目的是根据不同的类型提供不同的差异算法。每个类型都有一个相应的差异策略。因此,如果应用类的配置不正确,我们会得到不可预知的结果。

To tell JaVers what type to use for a class, we have several options:

要告诉JaVers为一个班级使用什么类型,我们有几个选项。

  • Explicitly – the first option is to explicitly use register* methods of the JaversBuilder class – the second way is to use annotations
  • Implicitly – JaVers provides algorithms for detecting types automatically based on class relations
  • Defaults – by default, JaVers will treat all classes as ValueObjects

In this tutorial, we’ll configure JaVers explicitly, using the annotation method.

在本教程中,我们将使用注释方法明确配置JaVers。

The great thing is that JaVers is compatible with javax.persistence annotations. As a result, we won’t need to use JaVers-specific annotations on our entities.

最棒的是,JaVers与javax.persistence注释兼容。因此,我们不需要在我们的实体上使用JaVers特定的注解。

4. Sample Project

4.项目样本

Now we’re going to create a simple application that will include several domain entities that we’ll be auditing.

现在我们要创建一个简单的应用程序,其中包括几个我们要审计的域实体。

4.1. Domain Models

4.1.领域模型

Our domain will include stores with products.

我们的领域将包括有产品的商店。

Let’s define the Store entity:

我们来定义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
}

Please note that we are using default JPA annotations. JaVers maps them in the following way:

请注意,我们使用的是默认的JPA注释。JaVers以如下方式映射它们。

  • @javax.persistence.Entity is mapped to @org.javers.core.metamodel.annotation.Entity
  • @javax.persistence.Embeddable is mapped to @org.javers.core.metamodel.annotation.ValueObject.

Embeddable classes are defined in the usual manner:

可嵌入类是以通常的方式定义的。

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

4.2. Data Repositories

4.2.数据存储库

In order to audit JPA repositories, JaVers provides the @JaversSpringDataAuditable annotation.

为了审计JPA存储库,JaVers提供了@JaversSpringDataAuditable注解。

Let’s define the StoreRepository with that annotation:

让我们用该注解定义StoreRepository

@JaversSpringDataAuditable
public interface StoreRepository extends CrudRepository<Store, Integer> {
}

Furthermore, we’ll have the ProductRepository, but not annotated:

此外,我们会有ProductRepository,但没有注解。

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

Now consider a case when we are not using Spring Data repositories. JaVers has another method level annotation for that purpose: @JaversAuditable.

现在考虑一下我们不使用Spring Data存储库的情况。JaVers有另一个方法级别的注解来实现这个目的。@JaversAuditable.

For example, we may define a method for persisting a product as follows:

例如,我们可以定义一个持久化产品的方法,如下所示。

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

Alternatively, we can even add this annotation directly above a method in the repository interface:

另外,我们甚至可以直接在资源库接口的方法上面添加这个注解。

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

4.3. Author Provider

4.3.作者提供者

Each committed change in JaVers should have its author. Moreover, JaVers supports Spring Security out of the box.

在JaVers中,每个提交的变更都应该有其作者。此外,JaVers支持Spring Security out of the box。

As a result, each commit is made by a specific authenticated user. However, for this tutorial we’ll create a really simple custom implementation of the AuthorProvider Interface:

因此,每次提交都是由一个特定的认证用户进行的。然而,在本教程中,我们将创建一个真正简单的AuthorProvider接口的自定义实现。

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

And as the last step, to make JaVers use our custom implementation, we need to override the default configuration bean:

而作为最后一步,为了让JaVers使用我们的自定义实现,我们需要覆盖默认的配置Bean。

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

5. JaVers Audit

5 JaVers审计

Finally, we are ready to audit our application. We’ll use a simple controller for dispatching changes into our application and retrieving the JaVers commit log. Alternatively, we can also access the H2 console to see the internal structure of our database:

最后,我们准备对我们的应用程序进行审计。我们将使用一个简单的控制器,将变化分派到我们的应用程序中,并检索JaVers的提交日志。另外,我们也可以访问H2控制台来查看我们数据库的内部结构。

H2 Console Google Chrome

 

 

 

 

 

 

 

To have some initial sample data, let’s use an EventListener to populate our database with some products:

为了有一些初始的样本数据,让我们使用一个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. Initial Commit

5.1.初始承诺

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

当一个对象被创建时,JaVers 首先做一个INITIAL类型的提交

Let’s check the snapshots after the application startup:

让我们检查一下应用程序启动后的快照。

@GetMapping("/stores/snapshots")
public String getStoresSnapshots() {
    QueryBuilder jqlQuery = QueryBuilder.byClass(Store.class);
    List<CdoSnapshot> 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:

在上面的代码中,我们正在为Store类查询JaVers的快照。如果我们向这个端点发出请求,就会得到如下的结果。

[
  {
    "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.

请注意,上面的快照包括所有添加到商店的产品,尽管缺少ProductRepository接口的注释

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

默认情况下,如果聚合根的所有相关模型与父模型一起被持久化,JaVers 将审核这些模型。

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

我们可以通过使用DiffIgnore注解告诉JaVers忽略特定的类。

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

例如,我们可以用Store实体中的注释来注释products字段。

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

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

因此,JaVers不会跟踪来自Store实体的产品变化。

5.2. Update Commit

5.2.更新承诺

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.

下一种提交类型是UPDATE提交。这是最有价值的提交类型,因为它代表了一个对象的状态变化。

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<Store> 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:

既然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);
}

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

我们会得到以前的INITIAL提交和新的UPDATE提交。

 {
    "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.

值得注意的是,JaVers并不创建新的数据库连接。相反,它重复使用现有的连接。在同一事务中,JaVers数据与应用程序数据一起被提交或回滚。

5.3. Changes

5.3.变化

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.

JaVers将变化记录为对象的版本之间的原子差异。我们可以从JaVers方案中看到,没有单独的表来存储变化,所以JaVers将变化动态地计算为快照之间的差异

Let’s update a product price:

让我们来更新一个产品的价格。

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

Then, let’s query JaVers for changes:

然后,让我们查询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);
}

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.

为了检测变化的类型,JaVers比较了一个对象的后续更新快照。在上面的案例中,由于我们改变了实体的属性,我们得到了PROPERTY_VALUE_CHANGED变化类型。

5.4. Shadows

5.4.影子

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.

此外,JaVers提供了另一种被审计实体的视图,称为Shadow。一个影子代表了从快照中恢复的对象状态。这个概念与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
  • Commit-deep — shadows are created from all snapshots related to selected entities
  • Deep+ — JaVers tries to restore full object graphs with (possibly) all objects loaded.

Let’s use the Child-value-object scope and get a shadow for a single store:

让我们使用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<Shadow<Store>> shadows = javers.findShadows(jqlQuery);
    return javers.getJsonConverter().toJson(shadows.get(0));
}

As a result, we’ll get the store entity with the Address value object:

结果是,我们将得到带有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": []
  }
}

To get products in the result we may apply the Commit-deep scope.

为了在结果中获得产品,我们可以应用Commit-deep范围。

6. Conclusion

6.结语

In this tutorial, we’ve seen how easily JaVers integrates with Spring Boot and Spring Data in particular. All in all, JaVers requires almost zero configuration to set up.

在本教程中,我们已经看到JaVers是如何轻松地与Spring Boot,特别是Spring Data集成的。总而言之,JaVers的设置几乎不需要任何配置。

To conclude, JaVers can have different applications, from debugging to complex analysis.

总而言之,JaVers可以有不同的应用,从调试到复杂分析。

The full project for this article is available over on GitHub.

本文的完整项目可在GitHub上获得over