Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 4535

Using JaVers for Data Model Auditing in Spring Data

$
0
0

1. Overview

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

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.

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

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.

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

3.1. Dependencies

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.

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

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

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

3.2. JaVers Repository Setup

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.

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

3.3. JaVers Properties

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

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

javers.newObjectSnapshot=true

3.4. JaVers Domain Configuration

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

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:

  • 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.

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.

4. Sample Project

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

4.1. Domain Models

Our domain will include stores with products.

Let's define the Store entity:

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

  • @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

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

Let’s define the StoreRepository with that annotation:

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

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

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.

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

Each committed change in JaVers should have its author. Moreover, JaVers supports 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:

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:

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

5. JaVers Audit

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:

H2 Console

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

@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

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

[
  {
    "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<Product> 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<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:

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

 {
    "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<Product> 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
  • 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:

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

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

6. Conclusion

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.

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

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


Viewing all articles
Browse latest Browse all 4535

Trending Articles