JaVers gebruiken voor datamodelaudits in Spring Data

1. Overzicht

In deze tutorial zullen we zien hoe we JaVers kunnen instellen en gebruiken in een eenvoudige Spring Boot-applicatie om wijzigingen van entiteiten bij te houden.

2. JaVers

Als we te maken hebben met veranderlijke gegevens, hebben we meestal alleen de laatste staat van een entiteit die in een database is opgeslagen. Als ontwikkelaars besteden we veel tijd aan het debuggen van een applicatie, het doorzoeken van logbestanden voor een gebeurtenis die een toestand heeft veranderd. Dit wordt nog lastiger in de productieomgeving wanneer veel verschillende gebruikers het systeem gebruiken.

Gelukkig hebben we geweldige tools zoals JaVers. JaVers is een auditlograamwerk dat helpt bij het volgen van wijzigingen van entiteiten in de applicatie.

Het gebruik van deze tool is niet beperkt tot alleen foutopsporing en controle. Het kan met succes worden toegepast om analyses uit te voeren, beveiligingsbeleid af te dwingen en ook het gebeurtenislogboek bij te houden.

3. Projectopzet

Om JaVers te gaan gebruiken, moeten we eerst de audit repository configureren voor blijvende snapshots van entiteiten. Ten tweede moeten we enkele configureerbare eigenschappen van JaVers aanpassen. Ten slotte bespreken we ook hoe u onze domeinmodellen correct configureert.

Maar het is vermeldenswaard dat JaVers standaard configuratie-opties biedt, dus we kunnen het bijna zonder configuratie gaan gebruiken.

3.1. Afhankelijkheden

Ten eerste moeten we de JaVers Spring Boot-starter-afhankelijkheid aan ons project toevoegen. Afhankelijk van het type persistentieopslag hebben we twee opties: org.javers: javers-spring-boot-starter-sql en org.javers: javers-spring-boot-starter-mongo. In deze tutorial gebruiken we de Spring Boot SQL-starter.

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

Aangezien we de H2-database gaan gebruiken, laten we ook deze afhankelijkheid opnemen:

 com.h2database h2 

3.2. JaVers Repository instellen

JaVers gebruikt een repository-abstractie voor het opslaan van commits en geserialiseerde entiteiten. Alle gegevens worden opgeslagen in het JSON-formaat. Daarom kan het een goede keuze zijn om een ​​NoSQL-opslag te gebruiken. Voor de eenvoud gebruiken we echter een H2 in-memory instance.

JaVers maakt standaard gebruik van een in-memory repository-implementatie en als we Spring Boot gebruiken, is er geen extra configuratie nodig. Verder tijdens het gebruik van Spring Data starters hergebruikt JaVers de databaseconfiguratie voor de applicatie.

JaVers biedt twee starters voor SQL- en Mongo-persistentiestapels. Ze zijn compatibel met Spring Data en vereisen standaard geen extra configuratie. We kunnen echter altijd de standaardconfiguratiebonen negeren: JaversSqlAutoConfiguration.java en JaversMongoAutoConfiguration.java respectievelijk.

3.3. JaVers Eigenschappen

JaVers staat het configureren van verschillende opties toe, hoewel de Spring Boot-standaardinstellingen in de meeste gevallen voldoende zijn.

Laten we er maar één negeren, newObjectSnapshot, zodat we momentopnames kunnen krijgen van nieuw gemaakte objecten:

javers.newObjectSnapshot = waar 

3.4. JaVers domeinconfiguratie

JaVers definieert intern de volgende typen: entiteiten, waardeobjecten, waarden, containers en primitieven. Sommige van deze termen komen uit DDD-terminologie (Domain Driven Design).

Het belangrijkste doel van het hebben van verschillende typen is om verschillende diff-algoritmen te bieden, afhankelijk van het type. Elk type heeft een bijbehorende diff-strategie. Als gevolg hiervan krijgen we onvoorspelbare resultaten als applicatieklassen onjuist zijn geconfigureerd.

Om JaVers te vertellen welk type we voor een les moeten gebruiken, hebben we verschillende opties:

  • Uitdrukkelijk - de eerste optie is om expliciet te gebruiken registreren* methoden van de JaversBuilder class - de tweede manier is om annotaties te gebruiken
  • Impliciet - JaVers biedt algoritmen voor het automatisch detecteren van typen op basis van klassenrelaties
  • Standaardwaarden - Standaard behandelt JaVers alle klassen als ValueObjects

In deze tutorial zullen we JaVers expliciet configureren met behulp van de annotatiemethode.

Het mooie is dat JaVers is compatibel met javax.persistence annotaties. Als gevolg hiervan hoeven we geen JaVers-specifieke annotaties te gebruiken voor onze entiteiten.

4. Voorbeeldproject

Nu gaan we een eenvoudige applicatie maken die verschillende domeinentiteiten omvat die we zullen controleren.

4.1. Domeinmodellen

Ons domein zal winkels met producten bevatten.

Laten we de Winkel entiteit:

@Entity openbare klasse Store {@Id @GeneratedValue privé int id; private String naam; @Embedded privé adresadres; @OneToMany (mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true) private List producten = nieuwe ArrayList (); // constructeurs, getters, setters}

Houd er rekening mee dat we standaard JPA-annotaties gebruiken. JaVers brengt ze op de volgende manier in kaart:

  • @ javax.persistence.Entity is toegewezen aan @ org.javers.core.metamodel.annotation.Entity
  • @ javax.persistence.Embeddable is toegewezen aan @ org.javers.core.metamodel.annotation.ValueObject.

Insluitbare klassen worden op de gebruikelijke manier gedefinieerd:

@Embeddable public class Address {privé String-adres; privé Geheel getal zipCode; }

4.2. Gegevensopslagplaatsen

Om JPA-repositories te controleren, biedt JaVers de @JaversSpringDataAuditable annotatie.

Laten we de StoreRepository met die annotatie:

@JaversSpringDataAuditable openbare interface StoreRepository breidt CrudRepository uit {}

Verder hebben we de Productopslagplaats, maar niet geannoteerd:

openbare interface ProductRepository breidt CrudRepository uit {}

Overweeg nu een geval waarin we geen Spring Data-repositories gebruiken. JaVers heeft voor dat doel een andere annotatie op methode-niveau: @JaversAuditable.

We kunnen bijvoorbeeld als volgt een methode definiëren om een ​​product persistent te maken:

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

Als alternatief kunnen we deze annotatie zelfs direct boven een methode in de repository-interface toevoegen:

openbare interface ProductRepository breidt CrudRepository uit {@Override @JaversAuditable S save (S); }

4.3. Auteur Provider

Elke gecommitteerde wijziging in JaVers zou een auteur moeten hebben. Bovendien ondersteunt JaVers Spring Security out of the box.

Als resultaat wordt elke commit gemaakt door een specifieke geauthenticeerde gebruiker. Voor deze zelfstudie maken we echter een heel eenvoudige aangepaste implementatie van het AuthorProvider Koppel:

private statische klasse SimpleAuthorProvider implementeert AuthorProvider {@Override public String bieden () {return "Baeldung Author"; }}

En als laatste stap, om JaVers onze aangepaste implementatie te laten gebruiken, moeten we de standaardconfiguratieboon negeren:

@Bean openbare AuthorProvider verstrekJaversAuthor () {retourneer nieuwe SimpleAuthorProvider (); }

5. JaVers Audit

Eindelijk zijn we klaar om onze applicatie te controleren. We gebruiken een eenvoudige controller om wijzigingen in onze applicatie te verzenden en het vastleglogboek van JaVers op te halen. Als alternatief hebben we ook toegang tot de H2-console om de interne structuur van onze database te zien:

Laten we voor wat eerste voorbeeldgegevens een EventListener om onze database te vullen met enkele producten:

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

5.1. Initiële toezegging

Wanneer een object wordt gemaakt, levert JaVers maakt eerst een vastlegging van de EERSTE type.

Laten we eens kijken naar de momentopnamen na het opstarten van de applicatie:

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

In de bovenstaande code vragen we JaVers om snapshots voor het Winkel klasse. Als we een verzoek indienen bij dit eindpunt, krijgen we een resultaat zoals hieronder:

[{"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": " adres "}," naam ":" Baeldung store "," id ": 1," products ": [{" entity ":" com.baeldung.springjavers.domain.Product "," cdoId ": 2}, {" entiteit ":" com.baeldung.springjavers.domain.Product "," cdoId ": 3}]}," modifiedProperties ": [" adres "," naam "," id "," producten "]," type ": "INITIAL", "version": 1}]

Merk op dat de momentopname hierboven bevat alle producten die aan de winkel zijn toegevoegd ondanks de ontbrekende annotatie voor de Productopslagplaats koppel.

Standaard controleert JaVers alle gerelateerde modellen van een geaggregeerde root als ze samen met de ouder worden behouden.

We kunnen JaVers vertellen om specifieke klassen te negeren door de DiffIgnore annotatie.

We kunnen bijvoorbeeld de producten veld met de annotatie in de Winkel entiteit:

@DiffIgnore private List-producten = nieuwe ArrayList ();

Daarom houdt JaVers geen wijzigingen bij van producten die afkomstig zijn van de Winkel entiteit.

5.2. Update Commit

Het volgende type commit is de BIJWERKEN plegen. Dit is het meest waardevolle commit-type aangezien het veranderingen van de staat van een object vertegenwoordigt.

Laten we een methode definiëren waarmee de winkelentiteit en alle producten in de winkel worden bijgewerkt:

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

Als we deze methode uitvoeren, krijgen we de volgende regel in de foutopsporingsoutput (in het geval van dezelfde producten en winkels):

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

Aangezien JaVers wijzigingen met succes heeft doorgevoerd, kunnen we de snapshots voor producten opvragen:

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

We krijgen vorige EERSTE commits en nieuw BIJWERKEN begaat:

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

Hier kunnen we alle informatie zien over de wijziging die we hebben aangebracht.

Het is vermeldenswaard dat JaVers maakt geen nieuwe verbindingen met de database. In plaats daarvan worden bestaande verbindingen opnieuw gebruikt. JaVers-gegevens worden gecommit of teruggedraaid samen met toepassingsgegevens in dezelfde transactie.

5.3. Veranderingen

JaVers registreert veranderingen als atomaire verschillen tussen versies van een object. Zoals we kunnen zien in het JaVers-schema, is er dus geen aparte tabel om wijzigingen op te slaan JaVers berekent veranderingen dynamisch als het verschil tussen snapshots.

Laten we een productprijs updaten:

openbare ongeldige updateProductPrice (geheel getal productId, dubbele prijs) {Optioneel productOpt = productRepository.findById (productId); productOpt.ifPresent (product -> {product.setPrice (prijs); productRepository.save (product);}); }

Laten we vervolgens JaVers vragen naar wijzigingen:

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

De uitvoer bevat de gewijzigde eigenschap en de waarden ervoor en erna:

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

Om een ​​type wijziging te detecteren, vergelijkt JaVers opeenvolgende snapshots van de updates van een object. In het bovenstaande geval, omdat we de eigenschap van de entiteit hebben gewijzigd, hebben we de PROPERTY_VALUE_CHANGED van type veranderen.

5.4. Schaduwen

Bovendien biedt JaVers een ander beeld van de gecontroleerde entiteiten die worden gebeld Schaduw. Een schaduw vertegenwoordigt een objectstatus die is hersteld op basis van momentopnamen. Dit concept hangt nauw samen met Event Sourcing.

Er zijn vier verschillende bereiken voor Shadows:

  • Oppervlakkig - schaduwen worden gemaakt op basis van een momentopname die is geselecteerd in een JQL-query
  • Kindwaarde-object - schaduwen bevatten alle onderliggende waarde-objecten die eigendom zijn van geselecteerde entiteiten
  • Commit-diep - schaduwen worden gemaakt van alle snapshots die betrekking hebben op geselecteerde entiteiten
  • Diep + - JaVers probeert volledige objectgrafieken te herstellen met (mogelijk) alle objecten geladen.

Laten we het bereik van het onderliggende waarde-object gebruiken en een schaduw krijgen voor een enkele winkel:

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

Als gevolg hiervan krijgen we de winkelentiteit met de Adres waarde 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 ":" Een straat "," zipCode ": 22222}, "producten": []}}

Om producten in het resultaat te krijgen, kunnen we de Commit-deep scope toepassen.

6. Conclusie

In deze tutorial hebben we gezien hoe gemakkelijk JaVers integreert met Spring Boot en Spring Data in het bijzonder. Al met al heeft JaVers bijna geen configuratie nodig om op te zetten.

Tot slot kan JaVers verschillende toepassingen hebben, van foutopsporing tot complexe analyse.

Het volledige project voor dit artikel is beschikbaar op GitHub.