Verwenden von JaVern für die Datenmodellprüfung in Frühlingsdaten

1. Übersicht

In diesem Tutorial erfahren Sie, wie Sie JaVers in einer einfachen Spring Boot-Anwendung einrichten und verwenden, um Änderungen an Entitäten zu verfolgen.

2. JaVers

Beim Umgang mit veränderlichen Daten wird normalerweise nur der letzte Status einer Entität in einer Datenbank gespeichert. Als Entwickler verbringen wir viel Zeit mit dem Debuggen einer Anwendung und durchsuchen Protokolldateien nach einem Ereignis, das einen Status geändert hat. Dies wird in der Produktionsumgebung noch schwieriger, wenn viele verschiedene Benutzer das System verwenden.

Glücklicherweise haben wir großartige Tools wie JaVers. JaVers ist ein Überwachungsprotokoll-Framework, mit dem Änderungen von Entitäten in der Anwendung verfolgt werden können.

Die Verwendung dieses Tools ist nicht nur auf das Debuggen und Überwachen beschränkt. Es kann erfolgreich angewendet werden, um Analysen durchzuführen, Sicherheitsrichtlinien zu erzwingen und das Ereignisprotokoll zu verwalten.

3. Projekteinrichtung

Um JaVers verwenden zu können, müssen Sie zunächst das Audit-Repository für persistente Snapshots von Entitäten konfigurieren. Zweitens müssen wir einige konfigurierbare Eigenschaften von JaVern anpassen. Abschließend erfahren Sie auch, wie Sie unsere Domänenmodelle richtig konfigurieren.

Es ist jedoch erwähnenswert, dass JaVers Standardkonfigurationsoptionen bietet, sodass wir es fast ohne Konfiguration verwenden können.

3.1. Abhängigkeiten

Zuerst müssen wir die JaVers Spring Boot-Starterabhängigkeit zu unserem Projekt hinzufügen. Abhängig von der Art des Persistenzspeichers haben wir zwei Optionen: org.javers: Javers-Spring-Boot-Starter-SQL und org.javers: Javers-Spring-Boot-Starter-Mongo . In diesem Tutorial verwenden wir den Spring Boot SQL-Starter.

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

Da wir die H2-Datenbank verwenden werden, schließen wir auch diese Abhängigkeit ein:

 com.h2database h2 

3.2. JaVers Repository Setup

JaVers verwendet eine Repository-Abstraktion zum Speichern von Commits und serialisierten Entitäten. Alle Daten werden im JSON-Format gespeichert. Daher ist es möglicherweise gut geeignet, einen NoSQL-Speicher zu verwenden. Der Einfachheit halber verwenden wir jedoch eine H2-In-Memory-Instanz.

Standardmäßig nutzt JaVers eine In-Memory-Repository-Implementierung. Wenn wir Spring Boot verwenden, ist keine zusätzliche Konfiguration erforderlich. Darüber hinaus verwendet JaVers bei Verwendung von Spring Data-Startern die Datenbankkonfiguration für die Anwendung erneut .

JaVers bietet zwei Starter für SQL- und Mongo-Persistenzstapel. Sie sind mit Spring Data kompatibel und erfordern standardmäßig keine zusätzliche Konfiguration. Wir können jedoch immer die Standardkonfigurations-Beans überschreiben: JaversSqlAutoConfiguration.java bzw. JaversMongoAutoConfiguration.java .

3.3. JaVers Eigenschaften

Mit JaVers können mehrere Optionen konfiguriert werden, obwohl die Spring Boot-Standardeinstellungen in den meisten Anwendungsfällen ausreichend sind.

Überschreiben wir nur einen, newObjectSnapshot , damit wir Snapshots von neu erstellten Objekten erhalten können:

javers.newObjectSnapshot=true 

3.4. JaVers Domain-Konfiguration

JaVers definiert intern die folgenden Typen: Entitäten, Wertobjekte, Werte, Container und Grundelemente. Einige dieser Begriffe stammen aus der DDD-Terminologie (Domain Driven Design).

Der Hauptzweck mehrerer Typen besteht darin, je nach Typ unterschiedliche Diff-Algorithmen bereitzustellen . Jeder Typ hat eine entsprechende Diff-Strategie. Wenn Anwendungsklassen falsch konfiguriert sind, erhalten wir daher unvorhersehbare Ergebnisse.

Um JaVers mitzuteilen, welcher Typ für eine Klasse verwendet werden soll, haben wir verschiedene Möglichkeiten:

  • Explizit - die erste Option ist die explizite Verwendung von register * -Methoden der JaversBuilder- Klasse - die zweite Möglichkeit ist die Verwendung von Anmerkungen
  • Implizit - JaVers bietet Algorithmen zum automatischen Erkennen von Typen basierend auf Klassenbeziehungen
  • Defaults - standardmäßig Javers alle Klassen wie behandeln valueobjects

In diesem Tutorial konfigurieren wir JaVers explizit mithilfe der Annotationsmethode.

Das Tolle ist, dass JaVers mit javax.persistence- Annotationen kompatibel ist . Daher müssen wir für unsere Entitäten keine JaVers-spezifischen Anmerkungen verwenden.

4. Beispielprojekt

Jetzt erstellen wir eine einfache Anwendung, die mehrere Domänenentitäten enthält, die wir prüfen werden.

4.1. Domänenmodelle

Unsere Domain wird Geschäfte mit Produkten umfassen.

Definieren wir die Store- Entität:

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

Bitte beachten Sie, dass wir Standard-JPA-Anmerkungen verwenden. JaVers ordnet sie folgendermaßen zu:

  • @ javax.persistence.Entity ist @ org.javers.core.metamodel.annotation.Entity zugeordnet
  • @ javax.persistence.Embeddable ist @ org.javers.core.metamodel.annotation.ValueObject zugeordnet.

Einbettbare Klassen werden wie folgt definiert:

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

4.2. Datenrepositorys

Um JPA-Repositorys zu prüfen, stellt JaVers die Annotation @JaversSpringDataAuditable bereit .

Let’s define the StoreRepository with that annotation:

@JaversSpringDataAuditable public interface StoreRepository extends CrudRepository { }

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

public interface ProductRepository extends CrudRepository { }

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 { @Override @JaversAuditable  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:

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 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
  • Commit-Deep - Schatten werden aus allen Schnappschüssen erstellt, die sich auf ausgewählte Objekte beziehen
  • Deep + - JaVers versucht, vollständige Objektdiagramme mit (möglicherweise) allen geladenen Objekten wiederherzustellen.

Verwenden wir den Child-Value-Object-Bereich und erhalten einen Schatten für einen einzelnen Speicher:

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

Als Ergebnis erhalten wir die Geschäftsentität mit dem Adresswertobjekt :

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

Um Produkte im Ergebnis zu erhalten, können wir den Commit-Deep-Bereich anwenden.

6. Fazit

In diesem Tutorial haben wir gesehen, wie einfach sich JaVers insbesondere in Spring Boot und Spring Data integrieren lässt. Alles in allem erfordert JaVers fast keine Konfiguration zum Einrichten.

Zusammenfassend lässt sich sagen, dass JaVer unterschiedliche Anwendungen haben können, vom Debuggen bis zur komplexen Analyse.

Das vollständige Projekt für diesen Artikel ist auf GitHub verfügbar.