CQRS tutorial (het Axon Framework)

 
09 april 2010

Gechreven door Jilles van Oossanen en Anatoly Zimakov

Deze blogpost beschrijft hoe een applicatie opgezet kan worden volgens de Command Query Segregation Responsibilty aanpak. Als ondersteunend framework wordt hierbij gebruik gemaakt van het Axon Framework. Dit framework biedt de mogelijkheid om de CQRS aanpak op verschillende manieren te implementeren in je applicatie. Voor deze blogpost is er voor gekozen om van deze verschillende manieren er één te beschrijven. Het is dus wel belangrijk te weten dat het framework meer mogelijkheden biedt.

Download de source code van de applicatie. Open de applicatie met NetBeans 6.8 (kies Java download bundle met GlassFish server). Om de applicatie draaiend te krijgen heb je ook een MySQL server nodig (bijvoorbeeld xampp). Maak een database ‘cqrssample’ aan en voeg een tabel ‘borrower’ toe. De SQL-script is te vinden in borrower.sql, welke zich in src/main/resources map van het project bevindt.

De applicatie is een uitleenysteem die alleen leners kan opslaan, meer niet. Dit is echter genoeg functionaliteit om de hele CQRS-cirkel rond te gaan en de belangrijkste aspecten van het framework te beschrijven.

Als je een event-driven applicatie bouwt, dan ben je waarschijnlijk geinsteresseerd hoe de afgevuurde events naar het buitenwereld verstuurd kunnen worden. Het Axon Framework biedt daar een oplossing voor: koppeling met Spring Integration. Om te laten zien wat de mogeljike koppeling met een externe applicatie kan zijn, hebben wij ook dit stuk geimplementeerd. Aan het eind van deze blogpost vertellen wij meer over hoe deze koppeling opgezet is.

Als de bovengenoemde stappen gedaan is, kunnen wij in de source code kijken om erachter te komen hoe de CQRS aanpak met behulp van het Axon Framework geimplementeerd kan worden.

GUI opzetten

In het ‘Web Pages’ onderdeel van de CQRSSample applicatie (de onderdelen van het project kan je zien in het ‘Projects’ view van NetBeans. Als deze onzichtbaar is, kun je hem zichtbaar maken door naar Window -> Projects te gaan) vind je de WEB-INF map, waarin de UI zich bevindt.

Voor de applicatie is gekozen voor een simpele JSP+Servlet oplossing voor de GUI. Deze technieken zijn gekozen omdat ze eenvoudig toe te passen zijn en dat ze doen wat gedaan moet worden. Het is dus goed mogelijk om andere technieken te gebruiken aangezien de GUI communiceert met de achterliggende code door simpel method calls.

In het ‘Web Pages’ vind je create.jsp. Dit bestand bevat eenvoudige HTML-formulier om de nieuwe lener aan te maken. De bestaande leners komen in het overzicht beneden in de pagina te staan. Submitten van het formulier roept ‘create’ actie aan. Deze actie is in deployment descriptor (WEB-INF/web.xml) gekoppeld aan de CreateBorrowerServlet (zie nl.sogyo.cqrssample.ui.servlets package).

Basis

De CreateBorrowerServlet verwerkt de ingevoerde gegevens (voornaam en achternaam van de lener) en roept rechtstreeks de CreateBorrowerCommandHandler (zie nl.sogyo.cqrssample.commandhandlers)  aan om vervolgens de nieuwe lener aan te maken. De instantie van CreateBorrowerCommandHandler wordt door de servlet vanuit de ‘application context’ geladen. Deze commandhandler wordt in application-context.xml als bean gedefineerd en samen met andere beans tijdens het opstarten van de applicatie door de Spring Framework geinitialiseerd.

Commandhandler opzetten

Zoals duidelijk zal zijn, wordt in een CQRS applicatie onderscheid gemaakt tussen Commands en Queries. Een command is een aanpassing aan de status van het systeem (create, update, delete). Een query is echter een vraag om data uit het systeem (read). Om deze commands af te handelen moeten hier CommandHandlers voor gemaakt worden. Deze vangen de commando’s op en sturen het domein aan om de commando’s vervolgens af te handelen.

Het maken van een commandhandler is relatief eenvoudig aangezien deze slechts een doorgeef luik van commando’s is. De klasse moet wel de interface Serializable implementeren. Daarnaast bevat het de methodes welke beschrijven wat er gedaan kan worden. In deze methodes worden dan weer calls gedaan naar methodes van aggregates in het domein. Een commandhandler mag dan ook geen logica bevatten, dit hoort namelijk echt thuis in het domein en de commandhandler bevindt zich meer tegen het domein aan.

Aggregates opzetten

Binnen de CQRS aanpak wordt een domein beschreven welke is opgezet volgens het Domain Driven Design. Deze architectuur beschrijft een aanpak waarmee je de business logica op de juiste plaats krijgt, in je domein. In dit design wordt ook gebruik gemaakt van de term Aggregates. Kort samengevat is een aggregate een samenvoeging van entiteiten welke nauw met elkaar verbonden zijn. Daarnaast zal een goede implementatie ervoor zorgen dat je geen getters of setters hoeft te gebruiken, omdat de entiteiten binnen een aggregate nauw verbonden zijn met elkaar. Wanneer aggregates onderling moeten communiceren, dan zijn de aggregates mogelijk niet goed opgezet. Eventuele communicatie tussen aggregates gaat namelijk altijd m.b.v. de events die de aggregates afvuren, maar hierover later meer.

Het maken van een aggregate gaat heel eenvoudig dankzij het Axon framework. Zoals in de source code (zie Borrower in nl.sogyo.cqrssample.aggregates package) te zien is, hoeft alleen de abstracte klasse AbstractAnnotatedAggregateRoot (onderdeel van het Axon Framework) geextend te worden. Daarna moet er nog wel een methode van deze klasse geïmplementeerd worden, namelijk createDeletedEvent(). Een mogelijke implementatie zie je hieronder. Daarnaast moeten er sowieso twee constructors toegevoegd worden, één waaraan een UUID wordt meegegeven. En één waaraan attributen worden meegegeven om het af te vuren event van data te voorzien.

Events opzetten

Om er achter te komen welke events je aan een aggregate moet toewijzen, moet simpelweg gekeken worden welke status veranderingen er kunnen optreden voor die aggregate. Het event beschrijft dan ook een verandering van die status. Het event BorrowerCreatedEvent (zie nl.sogyo.cqrssample.events package) beschrijft voor het aggregate Borrower dat er een nieuwe instantie van is aangemaakt. In het event kan dan data worden meegegeven welke in het domein verwerkt kan worden, denk hierbij aan het id van de aggregate en bijvoorbeeld de voornaam van de lener.

Voor het maken van een event moet de abstracte klasse DomainEvent (onderdeel van het Axon Framework) geextend worden. Wanneer dat gedaan is, moeten de attributen toegevoegd worden welke de daadwerkelijke statusveranderingen kunnen bevatten. Het is verstandig om deze attributen final te maken aangezien de data in een event na invulling nooit meer aangepast zou mogen worden.

Nu het event aangemaakt is, kan de aggregate waar dit event betrekking op heeft, aangepast worden. In de constructor waaraan de attributen worden meegegeven moet een call naar de overgeërfde methode apply worden toegevoegd (zie constructor van de Borrower). Deze methode wilt echter wel een domainevent als argument ontvangen. Aangezien het hier om de constructor gaat, moet dus het BorrowerCreatedEvent als event meegegeven worden. Deze apply methode zal er voor zorgen dat dit event geregistreerd wordt bij zijn aggregate en uiteindelijk afgevuurd kan worden, maar hier meer over in het volgende onderdeel.

Repository opzetten

Binnen de CQRS aanpak is de repository (zie BorrowerRepository in nl.sogyo.cqrssample.repositories package) verantwoordelijk voor het opslaan van events. Op deze manier wordt er dus altijd een zeer gedetailleerd log bijgehouden van de gebeurtenissen binnen je applicatie. Deze Event-source zou bijvoorbeeld gebruikt kunnen worden wanneer het systeem om wat voor reden dan ook zijn data kwijt is. Door alle events van de event-source dan uit te lezen kan de meest recente status weer bereikt worden.

Ook het opzetten van een repository is zeer eenvoudig d.m.v. het extenden van de abstracte klasse EventSourcingRepository (onderdeel van het Axon Framework). Op de plek van de ‘T’ moet de naam van de aggregate komen waar deze repository voor geldt. Na het extenden moet er echter wel nog twee methoden geïmplementeerd worden: instantiateAggregate() en getTypeIdentifier().

EventEnRepository

Wanneer de repository aangemaakt is, moeten we er ook voor zorgen dat hij daadwerkelijk gebruikt gaat worden in onze applicatie. Dit is heel eenvoudig door de repository toe te voegen aan de CreateBorrowerCommandHandler. In de code is te zien dat de repository gebruikt wordt bij het opslaan van een nieuwe instantie van een aggregate. In feite worden hiermee de nog niet opgeslagen events van deze aggregate opgeslagen in de repository.

Het BorrowerRepository object wordt ook in ‘application-context’ gedefineerd en het Spring Framework zorgt ervoor dat het object beschikbaar gesteld wordt voor de applicatie.

EventListeners opzetten

Zoals al eerder beschreven is, zijn eventlisteners de klassen welke aan de eventbus gekoppeld zijn en, voor hen, interessante events opvangen. Het is deze aanpak die ervoor zorgt dan de CQRS aanpak binnen de event-driven architecture valt. Door deze losse koppeling, via een eventbus, is het makkelijk mogelijk om componenten te koppelen zonder dat andere componenten hier last van ondervinden.

EventStoreEnReporting

Voor deze applicatie wordt gebruik gemaakt van een reporting database. Data uit de events wordt hierin opgeslagen, zodat de GUI zeer eenvoudig queries kan uitvoeren op deze database om zo aan zijn data te komen. Deze reporting database is onderdeel van het reporting component. De eventlistener van dit component gaat hier beschreven worden.

Het maken van een eventlistener moet in een aantal stappen gedaan worden (zie ReportingEventListener. java in nl.sogyo.cqrssample.reporting.eventlisteners) . Allereerst moet de abstracte klasse AbstractTransactionalEventListener (deze klasse is ontwikkeld door de makers van het Axon Framework maar behoort niet tot het framework zelf. Je zou eventueel een eigen transactional listener kunnen schrijven) geextend worden. AbstractTransactionalEventListener bevindt zich in dezelfde package als ReportingEventListener. Daarnaast moet er ook een annotatie aangebracht worden: @ConcurrentEventListener (met een sequencepolicy) om aan te geven hoe de events afgehandeld moeten worden.

Wanneer de klasse gemaakt is, kan de eventhandler methode worden toegevoegd. Dit is heel eenvoudig te doen door een methode te maken met als naam onBorrowerCreatedEvent welke als argument het BorrowerCreatedEvent mee krijgt. Om Spring te vertellen dat deze methode het event moet ontvangen vanuit de eventbus, wordt de methode geannoteerd met @EventHandler. Deze methode is nu in staat om het event af te handelen wanneer deze afgevuurd wordt.

In het geval van deze klasse zal deze methode moeten reageren op een BorrowerCreatedEvent. Dit houdt dus in dat hij de data uit het event moet opslaan in de reporting database. Voor deze applicatie hebben we hiervoor gebruikt gemaakt van de SimpleJdbc functionaliteit van Spring. Om hier gebruik van te maken, moet er een SimpleJdbcTemplate relatie aangelegd worden (zie application context voor configuratie)

QueryResults opzetten

In het volgende onderdeel wordt beschreven hoe een BorrowerQueryService daadwerkelijk data kan ophalen uit de reporting database. Deze data moet echter wel in een bepaalde vorm bij de GUI komen. Hier komen de QueryResult objecten om de hoek kijken.

Een queryresult object is niks anders dan een simpele DTO waarin de resultaten van de query zich bevinden. Wanneer er eerst goed wordt nagedacht over de structuur van deze objecten (aan de hand van de GUI) wordt het straks eenvoudiger om de select statements te bepalen voor de QueryService (zie BorrowerQueryResult in nl.sogyo.cqrssample.reporting.queries package).

QueryService opzetten

Nu er data weggeschreven kan worden naar de reporting tool, is het natuurlijk interessant om ook te kijken hoe deze data door de GUI opgehaald kan gaan worden. Met behulp van een zogeheten BorrowerQueryService is het mogelijk om gebruik te maken van de database (zie BorrowerQueryService in nl.sogyo.cqrssample.reporting.queries package). Waar wel op gelet moet worden, is dat deze database alleen de mogelijkheid biedt om gegevens uit te lezen en dat er dus niks toegevoegd, aangepast of verwijderd mag worden vanuit de GUI. Deze database kan voor het uitlezen van gegevens geoptimaliseerd worden.

Om de implementatie van je methodes in de BorrowerQueryService af te schermen van de GUI, is het verstandig om een interface te bouwen welke aangeeft welke methodes er worden aangeboden. Deze interface moet vervolgens geïmplementeerd worden door een klasse welke dan in zijn implementatie daadwerkelijk kan communiceren met de database. Deze communicatie gebeurt ook hier weer via de SimpleJdbc functionaliteit van Spring. Wanneer deze service gemaakt is kan hij toegevoegd worden aan de GUI. Zoals de instantie van de CreateBorrowerCommandHandler, wordt ook de instantie van de BorrowerQueryService in de init() methode van de CreateBorrowerServlet vanuit de application context opgehaald. Vervolgens worden alle leners met behulp van de query service in de processRequest() opgehaald en in de request scope van de servlet gezet.

Koppelen externe applicatie

Wanneer de queryservice gerealiseerd is zou er een werkende applicatie moeten staan. Dit hier aan toe zijn we uitgegaan van een eventhandler welke zich binnen dezelfde applicatie bevindt. Het is echter realistischer dat een eventhandler zich daadwerkelijk bevindt in een andere applicatie. In dit onderdeel zal behandeld worden hoe het mogelijk is om een externe applicatie te koppelen aan de eventbus. Hiervoor moet gebruik gemaakt worden van de Spring Integration functionaliteit van het Axon framework.

Om een externe applicatie te koppelen wordt gebruik gemaakt van webservices. Met behulp van het Spring Integration framework wordt er dan een eventbus gerealiseerd, welke met zogeheten channels kan communiceren met webservices van externe applicaties.

ExternalApp

Allereerst moeten we dus een externe applicatie hebben welke webservices aanbiedt. In ons geval wordt gebruik gemaakt van een Spring Webservices applicatie. De opbouw van deze applicatie wordt niet verder behandeld, omdat het niet binnen de scope van de blogpost is.

Aangezien de externe applicatie ook response stuurt, moet het type van response object ook aan de kant van onze CQRSSample applicatie bekend zijn. De applicatie moet ook in staat zijn om de te versturen requests te serialiseren en responses van de externe applicatie te deserialiseren (marshalling/unmarshalling). Zoals de externe applicatie, maakt de CQRSSample applicatie ook gebruik van JiBX mapper om (un-)marshalling mogelijk te maken. JiBX moet weten hoe de te versturen events en te ontvangen responses naar en vanuit XML geserialiseerd worden. Dit wordt in een binding.xml bestand gedefinieerd (zie nl.sogyo.cqrssample.ws.bindings).

Bovendien moeten wij in de CQRSSample applicatie de EventResponse klasse defineren om het response object van de externe applicatie locaal bekend te maken (zie nl.sogyo.cqrssample.ws.messages).

Wanneer al deze stappen ondernomen zijn, moet communicatie mogelijk zijn. Wanneer er nu een BorrowerCreatedEvent opgegooid wordt, zal de externe applicatie een response moeten geven.

Standaard maakt de CQRSSample applicatie geen gebruik van Spring Integration. Events worden op gewone EventBus van het Axon Framework gezet. Als je events naar buiten wil sturen, laat de applicatie configuratie uit het application-context-integration.xml bestand gebruiken. Verwijzing naar de application context wordt als context-param in web.xml ingesteld.


Werken met ?
Kijk dan bij onze mogelijkheden voor starters en/of ervaren IT'ers.


Categorieën: Development