In dit artikel lees je meer over hoe je versiebeheer kunt doen op entiteiten. Het bijhouden van data wijzigen in de breedste zin van het woord. Dit is een van de requirements die een project binnen sluipt met een simpele requirement, zoals de historie moet bewaard worden en later steeds meer uitgebreid moet worden tot aan auditing toe. Als je dit zelf ontwikkelt, kom je erachter dat je onder andere ook iets moet doen met relaties van de entiteit waar je versiebeheer op toepast. Voor je het weet heb je veel code geschreven voor een suboptimale oplossing. Na een introductie over de mogelijkheden en beperkingen ga ik aan de hand van een case de inrichting en gebruik uiteenzetten.
Auteur: Ben Ooms
In dit artikel maken we gebruik van een library waar je eenvoudig versiebeheer kan toepassen op een Spring project waar je gebruikmaakt van JPA en Spring Data. Het is belangrijk om te weten dat je enkel gebruikmaakt van JPA op de entiteiten waar je het versiebeheer op wilt doen, want deze library zal geen wijzigingen kunnen bijhouden waarbij andere mechanismen zijn gebruikt om entiteiten te persisteren zoals JDBC. Mocht je dit wel willen, of versiebeheer willen kunnen uitvoeren op NOSQL databases, raad ik je aan te kijken naar een andere library genaamd Javers. Het versiebeheer en auditing wordt alleen toegepast op de entiteiten die je expliciet annoteert.
Envers
Envers is een library die je met hibernate gebruikt. Envers maakt gebruik van het JPA event systeem en vangt events van wijzigingen op en past dan het versiebeheer toe door historie op te slaan in historie tabellen. Dit kun je instellen op de tabellen waar je versiebeheer op wilt toepassen en kun je zelfs op veldniveau instellen.
Voor het Auditing deel maken we gebruik van functionaliteit van Spring Data. Deze biedt annotaties en een interface aan die je moet implementeren om de auditing velden te vullen wanneer deze opgeslagen worden.
Om Envers te kunnen gebruiken op een Spring Boot Data project, heb je maar een dependency nodig en dat is org.springframework.data:spring-data-envers. Deze implementatie biedt een aantal geïmplementeerde Envers interfaces aan met een Factory bean om Envers specifieke read-only Jpa Repositories aan te maken. Tip: als je geen gebruikmaakt van Spring Data, kun je in de sources van dit project inspiratie op doen om Envers in een andere JPA omgeving te gebruiken.
Na deze dependencies in je project opgenomen te hebben, kun je aan de slag om versiebeheer en auditing toe te passen op entiteiten. Voordat ik dat doe wil ik eerst de case toelichten die als voorbeeld zal dienen. De applicaties gebruikt een aantal Spring Boot starters (web, validation, actuator, data en test) en twee losse dependencies (H2 voor persistentie en Lombok voor gemak ;>). Dit zijn gebruikelijke dependencies voor een Crud gebaseerde Rest service voor een relationeel model obv Spring Boot. De case is een heel simpel domeinmodel dat bestaat uit drie entiteiten bestaande uit Inventory, Product en Category. De code voor de basis is getagged als “Spring-rest-no-versioning”. Op afbeelding 1 zie je een ERD van de database die JPA heeft gegenereerd op basis van deze drie entiteiten.
De volgende requirements gaan wij implementeren:
- Volledige versiebeheer op de inventory entiteit op alle velden
- Versiebeheer op de productvelden name en price
- Audit informatie op producten en categories
Daarnaast willen wij weten wie een inventory item heeft toegevoegd en aangepast, inclusief de wijzigingen en de categorie wie de laatste wijziging heeft doorgevoerd.
Versiebeheer
We beginnen met het versiebeheer. Als eerste moeten weeen dependency toevoegen, dit is org.springframework.data:spring-data-envers:2.4.2. Met deze dependency halen wij zowel Envers als een aantal Spring klassen binnen voor versiebeheer.
Ik gebruik de schemageneratie van JPA, dit betekent dat tijdens het genereren van het model op basis van de Envers annotaties ook de historie tabellen gegenereerd worden. Ik zal hierbij alle defaults gebruiken waarbij in het laatste deel van het artikel interessante configuratie opties zal bespreken.
Om versiebeheer toe te passen, voeg je de @Audited annotatie toe aan de klasse of aan velden van een klasse. Om dit toe te passen in onze applicatie, voegen wij de annotatie toe op de Inventory entiteit op klasse niveau. Dit betekent dat alle velden van deze klasse onder versiebeheer staan en in de historie opgenomen zullen worden. Als je dit toepast en de Testen uitvoert zal je zien dat deze falen op het feit dat de applicatie context niet gestart kan worden. Dit komt doordat de Inventory entiteit een relatie met product heeft en Product geen versiebeheer heeft. Dit kun je op twee verschillende manieren oplossen afhankelijk van de requirements. Mocht je geen versiebeheer willen op de entiteit waar een relatie mee is, dan kun je dit op veldniveau aangeven door een @Audited annotatie te plaatsen en een parameter targetAuditMode met als waarde NOT_AUDITED toe te voegen. In dit stadium van ontwikkeling zetten wij een @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) toe. Goed om te weten is dat dit alleen een aanduiding is dat Product niet onder versiebeheer valt, er is nog degelijk wel versiebeheer op het veld productId.
In deze versie van de applicatie is het gegenereerde data model uitgebreid met een nieuwe tabel genaamd REVINFO en INVENTORY_AUD. Op afbeelding 2 zie je een schema met de nieuwe tabellen. De REVINFO tabel wordt door Envers gebruikt om en de INVENTORY_AUD voor het versiebeheer van de Inventory entiteit. Naast de velden van de Inventory entiteit worden ook andere velden toegevoegd, dit zijn standaard velden die Envers toevoegt. De REV geeft de revisienummer terug waarbij de REVTYPE het type mutatie beschrijft, dit kan zijn Insert, Update of Delete. De waarden die gebruikt worden zijn beschreven in de RevisionType enumeratie van de Envers library. Daarnaast vind je in de InventoryHistoryTests ook de verificatie dat er versiebeheer wordt toegepast en dat ondanks het Product geannoteerd is met @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) het wijzigen van een product in de inventory terug is te vinden in de historie. De code tot dit punt kun je vinden door de tag “versioning-done-for-inventory” uit te checken.
Om onze versiebeheer requirements te voltooien, moet er nog versiebeheer op de Product entiteit worden toegepast. Om dit te doen gaan wij voor de volledigheid gebruik maken van het annoteren op veldniveau. Wij zetten alleen de annotaties op de name en price velden. Daarnaast kunnen wij de annotatie op het veld product van de Inventory entity verwijderen want er is nu versiebeheer op producten, tenminste dat is wat je zou denken. Als je dit doet dan zie je dat de eerder geschreven testen zullen falen ook al laat je de annotatie staan. De reden hiervoor is dat er wel versiebeheer is op Product maar alleen op de velden name en price. Wanneer de revisie informatie opgehaald wordt voor de Inventory entity, wordt ook de bijhorende Product revisiehistorie gebruikt. Omdat er geen versiebeheer wordt toegepast op de description veld is deze null en falen testen op de conditie dat het product equals het gebruikte product. Hiervoor moeten de bestaande testen van de Inventory entity aangepast worden. Na dit gedaan te hebben zien wij het uiteindelijke model zoals getoond op afbeelding 3. Daar zie je de nieuwe historie tabel voor producten met alleen de product entiteit velden name en price. Na het toevoegen en aanpassen van een aantal testen zijn de versiebeheer requirements geïmplementeerd. De code tot dit punt is in Git getagged met “versioning-done”. Deze tag zal de basis vormen voor het laatste onderdeel, auditing.
Configuratiemogelijkheden van Envers
Voordat wij verder gaan met Spring auditing is het goed om stil te staan bij de verschillende configuratiemogelijkheden van Envers. Zonder specifieke configuratie op te geven kun je snel aan de slag door het versiebeheer te activeren met een annotatie op applicatieniveau en grof gezegd te strooien met @Audited annotaties op je entities. Je kunt veel onderdelen van Envers configureren zoals bijvoorbeeld de suffix voor de historie tabellen (default _AUD) en kolomnamen voor revisietype en revisienummer.
Daarnaast zijn er nog twee interessante configuratie opties die je kunt gebruiken. De eerste is de optie om wijzigingen op veldniveau bij te houden. Use case hiervoor is wanneer je ook informatie wilt opslaan over welke velden aangepast zijn. Elk veld in versiebeheer krijgt er met deze optie een boolean kolom bij om aan te geven of een veld aangepast is in de bewuste kolom. Als je deze optie globaal wilt gebruiken kun je in jouw application.properties de property org.hibernate.envers.global_with_modified_flag met de waarde true opnemen en op Klasse of veld niveau de property “withModifiedFlag” toe te voegen aan de @Audited annotatie.
Een andere interessante optie is de gebruikte strategie voor het opslaan van de historie.
Met de standaard strategie wordt er bij iedere wijziging een nieuwe record toegevoegd aan de historie. Dit heeft als voordeel dat de update operatie snel is, maar heeft wel als nadeel dat het ophalen van historie subqueries moeten worden uitgevoerd die impact hebben op de performance. Als je de validityAuditStrategie gebruikt wordt tijdens de insert van een nieuwe versie ook alle bestaande historie records bijgewerkt met een referentie naar de laatste records. Dit heeft als voordeel dat het ophalen van de historie geen subqueries behoeft en daardoor sneller is, nadeel is dan dat het updaten van de historie meer tijd neemt. Afhankelijk van de use case kun je voor een strategie kiezen, Haal je vaak de historie op, bijvoorbeeld om standaard historie te tonen, kun je beter de validityAuditStrategie gebruiken. Doe je dit op afroep is de standaard oplossing afdoende.
Goed om te weten is dat je Envers vergaand kunt aanpassen aan jouw wensen. In dit artikel raak ik maar een paar opties aan. Ook is het goed om vooraf goed na te denken over wat je precies wilt met de historie en te kijken naar de volatiliteit van de entities waar je versiebeheer op wilt toepassen want de historie tabellen kunnen zeer groot worden. Daarnaast wil je ook indexen toevoegen op relevante kolommen om de performance goed te houden.
Audit informatie toevoegen
Wij zijn nu toe aan de laatste requirement, het toevoegen van audit informatie op Producten en Categorieën. Hiervoor moeten wij als eerste auditing activeren, dit doe je door @EnableJpaAuditing op een Configuratie klasse toe te voegen zoals de Bootstrap klasse die geannoteerd is met @SpringBootApplication. Hierna hebben wij een bean nodig die Spring zal aanroepen wanneer de waarden geïnjecteerd worden voor het opslaan. Om deze te implementeren maak je een Bean aan die AuditorAware implementeert. Hiervoor moet je de methode getCurrentAuditor implementeren die de waarde levert onder welke naam de wijziging is aangebracht. Voor de hand liggend gebruik je de user gegevens die je uit de SecurityContext kunt verkrijgen en bij systeem bewerkingen zoals bijvoorbeeld een batch kunt je de gegevens voor auditing gewoon zetten. In onze voorbeeld applicatie geeft deze methode iedere keer de String “Applicatie” terug. Naast deze bean heb je enkel nog annotaties toe te voegen. Spring biedt vier annotaties hiervoor, dit zijn @CreatedBy, @LastModifiedBy, @CreatedDate en @LastModifiedDate. Om onze laatste requirement te implementeren voegen wij deze annotaties toe aan de Product en Category entiteiten. Om het DRY te houden extenden deze klassen nu de AbstractAuditedEntity klasse die de auditing annotaties bevat. Als laatste moeten wij nog een Entity listener opgeven die de velden voor ons gaat vullen, Dit doen wij op klasse niveau door de annotatie @EntityListeners(AuditingEntityListener.class) toe te voegen. De meegegeven AuditingEntityListener is een standaard listener uit Spring Data die de auditing velden zal vullen. Op afbeelding 4 zie je het uiteindelijke model waarbij zowel het versiebeheer en de auditing geïmplementeerd is.
Ook aan de auditing kun je veel configureren waarbij wij in de voorbeeld applicatie al een bean hebben geconfigureerd die (vrij nutteloos in de praktijk) altijd “Gebruiker” retourneert. Naast deze provider kun je ook eigen listeners implementeren. Wanneer je zowel de creatie als wijzigingen audit wordt standaard bij creatie ook de wijziging velden gevuld, dit kun je door middel van een property aanpassen.
In dit artikel hebben wij door middel van een voorbeeldapplicatie de werking van versiebeheer met Envers en Auditing met Spring Data uiteen gezet. Met de behandelde onderdelen en opties kun je een goede basis opzetten voor versiebeheer en auditing. Zowel Envers en Spring Data bieden veel aanpasmogelijkheden die niet behandeld zijn in dit artikel. Neem vooral een kijkje in de Github repository, alle behandelde onderwerpen zijn uitgewerkt en er zijn testen die de werking bewijzen. Ook kun je dit project gebruiken om verder te spelen met deze libraries. De broncode is te vinden op Github.
Referenties
https://sunitc.dev/2020/01/21/spring-boot-how-to-add-jpa-hibernate-envers-auditing/
https://hibernate.org/orm/envers/
https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#envers