CQRS en Event Sourcing in Java

1. Inleiding

In deze zelfstudie verkennen we de basisconcepten van Command Query Responsibility Segregation (CQRS) en Event Sourcing-ontwerppatronen.

Hoewel we vaak worden aangehaald als complementaire patronen, zullen we proberen ze afzonderlijk te begrijpen en uiteindelijk zien hoe ze elkaar aanvullen. Er zijn verschillende tools en frameworks, zoals Axon, om deze patronen te helpen adopteren, maar we zullen een eenvoudige applicatie in Java maken om de basisprincipes te begrijpen.

2. Basisconcepten

We zullen deze patronen eerst theoretisch begrijpen voordat we proberen ze te implementeren. Omdat ze vrij goed als individuele patronen staan, zullen we proberen het te begrijpen zonder ze te vermengen.

Houd er rekening mee dat deze patronen vaak samen worden gebruikt in een bedrijfstoepassing. In dit opzicht profiteren ze ook van verschillende andere enterprise-architectuurpatronen. We zullen er enkele van bespreken terwijl we verder gaan.

2.1. Sourcing van evenementen

Sourcing van evenementen geeft ons een nieuwe manier om de applicatiestatus te behouden als een geordende opeenvolging van gebeurtenissen. We kunnen deze gebeurtenissen selectief opvragen en de toestand van de applicatie op elk moment reconstrueren. Om dit te laten werken, moeten we natuurlijk elke wijziging in de staat van de applicatie opnieuw imiteren als gebeurtenissen:

Deze evenementen hier zijn feiten die zijn gebeurd en die niet kunnen worden gewijzigd - met andere woorden, ze moeten onveranderlijk zijn. Het opnieuw creëren van de applicatiestatus is slechts een kwestie van alle gebeurtenissen opnieuw afspelen.

Merk op dat dit ook de mogelijkheid biedt om gebeurtenissen selectief opnieuw af te spelen, sommige gebeurtenissen omgekeerd af te spelen en nog veel meer. Als gevolg hiervan kunnen we de applicatiestatus zelf als een secundaire burger behandelen, met het gebeurtenissenlogboek als onze primaire bron van waarheid.

2.2. CQRS

Simpel gezegd, CQRS is over het scheiden van de opdracht- en vraagzijde van de applicatiearchitectuur. CQRS is gebaseerd op het Command Query Separation (CQS) -principe dat werd voorgesteld door Bertrand Meyer. CQS suggereert dat we de bewerkingen op domeinobjecten in twee verschillende categorieën verdelen: Query's en Commands:

Query's retourneren een resultaat en veranderen de waarneembare status niet van een systeem. Commando's veranderen de toestand van het systeem, maar geven niet noodzakelijk een waarde terug.

We bereiken dit door de Command- en Query-kanten van het domeinmodel netjes te scheiden. We kunnen nog een stap verder gaan en natuurlijk ook de schrijf- en leeskant van de datastore splitsen door een mechanisme te introduceren om ze gesynchroniseerd te houden.

3. Een eenvoudige applicatie

We beginnen met het beschrijven van een eenvoudige applicatie in Java die een domeinmodel bouwt.

De applicatie biedt CRUD-bewerkingen op het domeinmodel en biedt ook een persistentie voor de domeinobjecten. CRUD staat voor Create, Read, Update en Delete, dit zijn basisbewerkingen die we kunnen uitvoeren op een domeinobject.

We zullen dezelfde applicatie gebruiken om Event Sourcing en CQRS in latere secties te introduceren.

In het proces zullen we enkele van de concepten uit Domain-Driven Design (DDD) in ons voorbeeld gebruiken.

DDD richt zich op de analyse en het ontwerp van software die steunt op complexe domeinspecifieke kennis. Het bouwt voort op het idee dat softwaresystemen gebaseerd moeten zijn op een goed ontwikkeld model van een domein. DDD werd voor het eerst voorgeschreven door Eric Evans als een catalogus van patronen. We zullen enkele van deze patronen gebruiken om ons voorbeeld te bouwen.

3.1. Toepassingsoverzicht

Het aanmaken en beheren van een gebruikersprofiel is een typische vereiste in veel toepassingen. We zullen een eenvoudig domeinmodel definiëren dat het gebruikersprofiel vastlegt, samen met een persistentie:

Zoals we kunnen zien, is ons domeinmodel genormaliseerd en stelt het verschillende CRUD-bewerkingen bloot. Deze operaties zijn alleen voor demonstratie en kan eenvoudig of complex zijn, afhankelijk van de vereisten. Bovendien kan de persistentierepository hier in het geheugen zijn of in plaats daarvan een database gebruiken.

3.2. Implementatie van applicaties

Eerst moeten we Java-klassen maken die ons domeinmodel vertegenwoordigen. Dit is een vrij eenvoudig domeinmodel en vereist misschien niet eens de complexiteit van ontwerppatronen zoals Event Sourcing en CQRS. We zullen dit echter eenvoudig houden om ons te concentreren op het begrijpen van de basisprincipes:

openbare klasse Gebruiker {privé String userid; private String voornaam; private String achternaam; privé Contacten instellen; privé ingestelde adressen; // getters and setters} public class Contact {private String type; privé String-detail; // getters and setters} public class Address {private String city; private String staat; privé String postcode; // getters en setters}

We zullen ook een eenvoudige opslagplaats in het geheugen definiëren voor de persistentie van onze applicatiestatus. Dit voegt natuurlijk geen waarde toe maar volstaat voor onze demonstratie later:

openbare klasse UserRepository {privé Map store = nieuwe HashMap (); }

Nu gaan we een service definiëren om typische CRUD-bewerkingen op ons domeinmodel bloot te leggen:

openbare klasse UserService {privé UserRepository-repository; openbare UserService (UserRepository-repository) {this.repository = repository; } public void createUser (String userId, String firstName, String lastName) {User user = nieuwe gebruiker (userId, firstName, lastName); repository.addUser (gebruikers-ID, gebruiker); } openbare ongeldige updateUser (String userId, Set contacten, Set adressen) {User user = repository.getUser (userId); user.setContacts (contacten); user.setAddresses (adressen); repository.addUser (gebruikers-ID, gebruiker); } openbaar Set getContactByType (String userId, String contactType) {User user = repository.getUser (userId); Stel contacten = user.getContacts (); retourneer contacts.stream () .filter (c -> c.getType (). is gelijk aan (contactType)) .collect (Collectors.toSet ()); } public Set getAddressByRegion (String userId, String state) {User user = repository.getUser (userId); Stel adressen in = user.getAddresses (); retourneer adressen.stream () .filter (a -> a.getState (). is gelijk aan (staat)) .collect (Collectors.toSet ()); }}

Dat is zo ongeveer wat we moeten doen om onze eenvoudige applicatie in te stellen. Dit is verre van productieklare code, maar het legt enkele van de belangrijke punten bloot waarover we later in deze tutorial gaan nadenken.

3.3. Problemen met deze toepassing

Voordat we verder gaan in onze discussie met Event Sourcing en CQRS, is het de moeite waard om de problemen met de huidige oplossing te bespreken. We zullen tenslotte dezelfde problemen aanpakken door deze patronen toe te passen!

Van de vele problemen die we hier kunnen opmerken, willen we ons graag op twee ervan concentreren:

  • Domeinmodel: De lees- en schrijfbewerkingen vinden plaats via hetzelfde domeinmodel. Hoewel dit geen probleem is voor een eenvoudig domeinmodel als dit, kan het erger worden naarmate het domeinmodel complex wordt. Mogelijk moeten we ons domeinmodel en de onderliggende opslag ervoor optimaliseren om aan de individuele behoeften van de lees- en schrijfbewerkingen te voldoen.
  • Volharding: De persistentie die we hebben voor onze domeinobjecten slaat alleen de laatste staat van het domeinmodel op. Hoewel dit voor de meeste situaties voldoende is, maakt het sommige taken uitdagend. Als we bijvoorbeeld een historische audit moeten uitvoeren om te zien hoe het domeinobject van status is veranderd, is dat hier niet mogelijk. Om dit te bereiken, moeten we onze oplossing aanvullen met enkele auditlogs.

4. Introductie van CQRS

We beginnen met het aanpakken van het eerste probleem dat we in de vorige sectie hebben besproken door het CQRS-patroon in onze applicatie te introduceren. Als onderdeel hiervan we zullen het domeinmodel en de persistentie ervan scheiden om schrijf- en leesbewerkingen af ​​te handelen. Laten we eens kijken hoe het CQRS-patroon onze applicatie herstructureert:

Het diagram hier legt uit hoe we van plan zijn om onze applicatiearchitectuur netjes te scheiden om zijden te schrijven en te lezen. We hebben hier echter nogal wat nieuwe componenten geïntroduceerd die we beter moeten begrijpen. Houd er rekening mee dat deze niet strikt gerelateerd zijn aan CQRS, maar CQRS heeft er veel baat bij:

  • Aggregaat / aggregator:

Aggregaat is een patroon beschreven in Domain-Driven Design (DDD) dat verschillende entiteiten logisch groepeert door entiteiten te binden aan een geaggregeerde root. Het geaggregeerde patroon zorgt voor transactionele consistentie tussen de entiteiten.

CQRS profiteert natuurlijk van het geaggregeerde patroon, dat het schrijffunctie-model groepeert en transactionele garanties biedt. Aggregaten houden normaal gesproken een cachestatus vast voor betere prestaties, maar kunnen perfect zonder deze status werken.

  • Projectie / projector:

Projectie is een ander belangrijk patroon dat CQRS enorm ten goede komt. Projectie betekent in wezen domeinobjecten in verschillende vormen en structuren vertegenwoordigen.

Deze projecties van originele gegevens zijn alleen-lezen en sterk geoptimaliseerd om een ​​verbeterde leeservaring te bieden. We kunnen opnieuw besluiten om projecties in het cachegeheugen op te slaan voor betere prestaties, maar dat is geen noodzaak.

4.1. Implementatie van de schrijfzijde van de applicatie

Laten we eerst de schrijfkant van de applicatie implementeren.

We beginnen met het definiëren van de vereiste commando's. EEN commando is een bedoeling om de toestand van het domeinmodel te muteren. Of het lukt of niet, hangt af van de bedrijfsregels die we configureren.

Laten we onze commando's eens bekijken:

openbare klasse CreateUserCommand {privé String userId; private String voornaam; private String achternaam; } public class UpdateUserCommand {private String userId; privé ingestelde adressen; privé Contacten instellen; }

Dit zijn vrij eenvoudige klassen die de gegevens bevatten die we willen muteren.

Vervolgens definiëren we een aggregaat dat verantwoordelijk is voor het nemen van opdrachten en het afhandelen ervan. Aggregaten kunnen een commando accepteren of weigeren:

openbare klasse UserAggregate {privé UserWriteRepository writeRepository; openbare UserAggregate (UserWriteRepository-repository) {this.writeRepository = repository; } openbare gebruiker handleCreateUserCommand (CreateUserCommand-commando) {User user = nieuwe gebruiker (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addUser (user.getUserid (), gebruiker); terugkeer gebruiker; } openbare gebruiker handleUpdateUserCommand (UpdateUserCommand-commando) {User user = writeRepository.getUser (command.getUserId ()); user.setAddresses (command.getAddresses ()); user.setContacts (command.getContacts ()); writeRepository.addUser (user.getUserid (), gebruiker); terugkeer gebruiker; }}

Het aggregaat gebruikt een repository om de huidige status op te halen en eventuele wijzigingen daarin vast te houden. Bovendien kan het de huidige status lokaal opslaan om de retourkosten naar een opslagplaats te vermijden tijdens het verwerken van elke opdracht.

Ten slotte hebben we een repository nodig om de staat van het domeinmodel vast te houden. Dit is meestal een database of een andere duurzame opslag, maar hier vervangen we ze eenvoudig door een datastructuur in het geheugen:

openbare klasse UserWriteRepository {privé Map store = nieuwe HashMap (); // accessors en mutators}

Dit concludeert de schrijfkant van onze applicatie.

4.2. Implementeren van Read Side of Application

Laten we nu overschakelen naar de leeszijde van de applicatie. We beginnen met het definiëren van de leeszijde van het domeinmodel:

openbare klasse UserAddress {privékaart addressByRegion = nieuwe HashMap (); } openbare klasse UserContact {privékaart contactByType = nieuwe HashMap (); }

Als we ons onze leesbewerkingen herinneren, is het niet moeilijk om te zien dat deze klassen perfect in kaart zijn om ze te verwerken. Dat is het mooie van het creëren van een domeinmodel waarin de vragen die we hebben centraal staan.

Vervolgens definiëren we de leesrepository. Nogmaals, we gebruiken alleen een datastructuur in het geheugen, ook al zal dit een duurzamere datastore zijn in echte applicaties:

openbare klasse UserReadRepository {privékaart userAddress = nieuwe HashMap (); privékaart userContact = nieuwe HashMap (); // accessors en mutators}

Nu gaan we de vereiste vragen definiëren die we moeten ondersteunen. Een query is een intentie om gegevens op te halen - het hoeft niet per se in gegevens te resulteren.

Laten we onze vragen eens bekijken:

openbare klasse ContactByTypeQuery {privé String userId; privé String contactType; } openbare klasse AddressByRegionQuery {privé String userId; private String staat; }

Nogmaals, dit zijn eenvoudige Java-klassen die de gegevens bevatten om een ​​query te definiëren.

Wat we nu nodig hebben, is een projectie die deze vragen aankan:

openbare klasse UserProjection {privé UserReadRepository readRepository; openbare UserProjection (UserReadRepository readRepository) {this.readRepository = readRepository; } public Set handle (ContactByTypeQuery-query) {UserContact userContact = readRepository.getUserContact (query.getUserId ()); retourneer userContact.getContactByType () .get (query.getContactType ()); } public Set handle (AddressByRegionQuery-query) {UserAddress userAddress = readRepository.getUserAddress (query.getUserId ()); retourneer userAddress.getAddressByRegion () .get (query.getState ()); }}

De projectie hier gebruikt de leesrepository die we eerder hebben gedefinieerd om de vragen die we hebben te beantwoorden. Dit concludeert vrijwel ook de leeszijde van onze applicatie.

4.3. Synchroniseren van lees- en schrijfgegevens

Een stukje van deze puzzel is nog steeds niet opgelost: er is niets aan te doen synchroniseer onze schrijf- en leesrepository's.

Dit is waar we iets nodig hebben dat bekend staat als een projector. EEN projector heeft de logica om het schrijfdomeinmodel in het gelezen domeinmodel te projecteren.

Er zijn veel meer geavanceerde manieren om hiermee om te gaan, maar we zullen het relatief eenvoudig houden:

openbare klasse UserProjector {UserReadRepository readRepository = nieuwe UserReadRepository (); openbare UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } public void project (User user) {UserContact userContact = Optioneel.ofNullable (readRepository.getUserContact (user.getUserid ())) .orElse (nieuwe UserContact ()); Kaart contactByType = nieuwe HashMap (); voor (Contactcontact: user.getContacts ()) {Contacten instellen = Optioneel.ofNullable (contactByType.get (contact.getType ())) .orElse (nieuwe HashSet ()); contacts.add (contactpersoon); contactByType.put (contact.getType (), contacten); } userContact.setContactByType (contactByType); readRepository.addUserContact (user.getUserid (), userContact); UserAddress userAddress = Optioneel.ofNullable (readRepository.getUserAddress (user.getUserid ())) .orElse (nieuw UserAddress ()); Kaart addressByRegion = nieuwe HashMap (); voor (Adresadres: user.getAddresses ()) {Stel adressen in = Optioneel.ofNullable (addressByRegion.get (address.getState ())) .orElse (nieuwe HashSet ()); adressen.add (adres); addressByRegion.put (address.getState (), adressen); } userAddress.setAddressByRegion (addressByRegion); readRepository.addUserAddress (user.getUserid (), userAddress); }}

Dit is nogal een heel grove manier om dit te doen, maar geeft ons voldoende inzicht in wat er nodig is zodat CQRS werkt. Bovendien is het niet nodig om de lees- en schrijfrepository's in verschillende fysieke winkels te hebben. Een gedistribueerd systeem heeft zo zijn eigen problemen!

Houd er rekening mee dat het het is niet handig om de huidige status van het schrijfdomein in verschillende leesdomeinmodellen te projecteren. Het voorbeeld dat we hier hebben genomen, is vrij eenvoudig, daarom zien we het probleem niet.

Naarmate de schrijf- en leesmodellen echter complexer worden, wordt het steeds moeilijker om te projecteren. We kunnen dit aanpakken door projectie op basis van gebeurtenissen in plaats van projectie op basis van staten met Event Sourcing. We zullen later in de tutorial zien hoe u dit kunt bereiken.

4.4. Voordelen en nadelen van CQRS

We bespraken het CQRS-patroon en leerden hoe we het in een typische applicatie konden introduceren. We hebben categorisch geprobeerd het probleem aan te pakken dat verband houdt met de starheid van het domeinmodel bij het omgaan met zowel lezen als schrijven.

Laten we nu enkele van de andere voordelen bespreken die CQRS met zich meebrengt voor een applicatiearchitectuur:

  • CQRS biedt ons een gemakkelijke manier om afzonderlijke domeinmodellen te selecteren geschikt voor schrijf- en leesbewerkingen; we hoeven geen complex domeinmodel te maken dat beide ondersteunt
  • Het helpt ons daarbij selecteer repositories die individueel geschikt zijn voor het omgaan met de complexiteit van de lees- en schrijfbewerkingen, zoals hoge doorvoer voor schrijven en lage latentie voor lezen
  • Het natuurlijk vormt een aanvulling op op gebeurtenissen gebaseerde programmeermodellen in een gedistribueerde architectuur door zowel een scheiding van zorgen als eenvoudigere domeinmodellen te bieden

Dit is echter niet gratis. Zoals uit dit eenvoudige voorbeeld blijkt, voegt CQRS een aanzienlijke complexiteit toe aan de architectuur. In veel scenario's is het misschien niet geschikt of de moeite waard:

  • Enkel en alleen een complex domeinmodel kan hiervan profiteren van de extra complexiteit van dit patroon; zonder dit alles kan een eenvoudig domeinmodel worden beheerd
  • Van nature leidt tot codeduplicatie tot op zekere hoogte, wat een aanvaardbaar kwaad is in vergelijking met de winst waartoe het ons leidt; Individueel oordeel wordt echter geadviseerd
  • Afzonderlijke opslagplaatsen leiden tot consistentieproblemen, en het is moeilijk om de schrijf- en leesrepository's altijd perfect synchroon te houden; we moeten vaak genoegen nemen met de uiteindelijke consistentie

5. Introductie van Event Sourcing

Vervolgens behandelen we het tweede probleem dat we in onze eenvoudige applicatie hebben besproken. Als we het ons herinneren, was het gerelateerd aan onze persistentierepository.

We introduceren Event Sourcing om dit probleem aan te pakken. Event Sourcing dramatisch verandert de manier waarop we denken over de opslag van de applicatiestatus.

Laten we eens kijken hoe het onze repository verandert:

Hier hebben we gestructureerd onze repository om een ​​geordende lijst met domeingebeurtenissen op te slaan. Elke wijziging aan het domeinobject wordt als een gebeurtenis beschouwd. Hoe grof of fijnmazig een evenement moet zijn, is een kwestie van domeinontwerp. De belangrijkste dingen die u hier moet overwegen, zijn dat gebeurtenissen hebben een tijdelijke volgorde en zijn onveranderlijk.

5.1. Implementeren van evenementen en evenementenwinkel

De fundamentele objecten in event-gestuurde applicaties zijn events, en event sourcing is niet anders. Zoals we eerder hebben gezien, gebeurtenissen vertegenwoordigen een specifieke verandering in de toestand van het domeinmodel op een bepaald tijdstip. We beginnen dus met het definiëren van de basisgebeurtenis voor onze eenvoudige applicatie:

openbare abstracte klasse Gebeurtenis {openbare definitieve UUID-id = UUID.randomUUID (); openbare definitieve datum gemaakt = nieuwe datum (); }

Dit zorgt ervoor dat elk evenement dat we in onze applicatie genereren een unieke identificatie krijgt en het tijdstempel van creatie. Deze zijn nodig om ze verder te kunnen verwerken.

Natuurlijk kunnen er verschillende andere attributen zijn die ons kunnen interesseren, zoals een attribuut om de herkomst van een gebeurtenis vast te stellen.

Laten we vervolgens enkele domeinspecifieke gebeurtenissen maken die overerven van deze basisgebeurtenis:

openbare klasse UserCreatedEvent breidt Event {private String userId uit; private String voornaam; private String achternaam; } public class UserContactAddedEvent breidt Event {private String contactType; privé String contactDetails; } public class UserContactRemovedEvent breidt Event {private String contactType; privé String contactDetails; } public class UserAddressAddedEvent breidt Event {private String city; private String staat; privé String postCode; } public class UserAddressRemovedEvent breidt Event {private String city; private String staat; private String postCode; }

Dit zijn eenvoudige POJO's in Java die de details van de domeingebeurtenis bevatten. Het belangrijkste om hier op te merken is echter de granulariteit van de gebeurtenissen.

We hadden één evenement kunnen maken voor gebruikersupdates, maar in plaats daarvan hebben we besloten om afzonderlijke evenementen te maken voor het toevoegen en verwijderen van adres en contactpersoon. De keuze wordt in kaart gebracht wat het efficiënter maakt om met het domeinmodel te werken.

Nu hebben we natuurlijk een repository nodig om onze domeingebeurtenissen te houden:

openbare klasse EventStore {privékaart store = nieuwe HashMap (); }

Dit is een eenvoudige datastructuur in het geheugen om onze domeingebeurtenissen vast te houden. In werkelijkheid, er zijn verschillende oplossingen die speciaal zijn gemaakt om gebeurtenisgegevens zoals Apache Druid te verwerken. Er zijn veel gedistribueerde datastores voor algemene doeleinden die in staat zijn om event sourcing te verwerken, waaronder Kafka en Cassandra.

5.2. Gebeurtenissen genereren en consumeren

Dus nu zal onze service die alle CRUD-bewerkingen heeft afgehandeld, veranderen. In plaats van een bewegende domeinstatus bij te werken, worden er domeingebeurtenissen toegevoegd. Het zal ook dezelfde domeingebeurtenissen gebruiken om op vragen te reageren.

Laten we eens kijken hoe we dit kunnen bereiken:

openbare klasse UserService {privé EventStore-opslagplaats; openbare UserService (EventStore-repository) {this.repository = repository; } public void createUser (String userId, String firstName, String lastName) {repository.addEvent (userId, new UserCreatedEvent (userId, firstName, lastName)); } openbare ongeldige updateUser (String userId, Set contacten, Set adressen) {User user = UserUtility.recreateUserState (repository, userId); user.getContacts (). stream () .filter (c ->! contacts.contains (c)) .forEach (c -> repository.addEvent (userId, nieuwe UserContactRemovedEvent (c.getType (), c.getDetail ()) )); contacts.stream () .filter (c ->! user.getContacts (). bevat (c)) .forEach (c -> repository.addEvent (userId, nieuwe UserContactAddedEvent (c.getType (), c.getDetail ()) )); user.getAddresses (). stream () .filter (a ->! adressen.contains (a)) .forEach (a -> repository.addEvent (userId, nieuw UserAddressRemovedEvent (a.getCity (), a.getState (), a.getPostcode ()))); adressen.stream () .filter (a ->! user.getAddresses (). bevat (a)) .forEach (a -> repository.addEvent (userId, nieuw UserAddressAddedEvent (a.getCity (), a.getState (), a.getPostcode ()))); } openbaar Set getContactByType (String userId, String contactType) {User user = UserUtility.recreateUserState (repository, userId); return user.getContacts (). stream () .filter (c -> c.getType (). equals (contactType)) .collect (Collectors.toSet ()); } public Set getAddressByRegion (String userId, String state) genereert Uitzondering {User user = UserUtility.recreateUserState (repository, userId); return user.getAddresses (). stream () .filter (a -> a.getState (). equals (state)) .collect (Collectors.toSet ()); }}

Houd er rekening mee dat we verschillende gebeurtenissen genereren als onderdeel van het afhandelen van de gebruikersupdate-bewerking hier. Het is ook interessant om op te merken hoe we zijn het genereren van de huidige status van het domeinmodel door alle tot dusver gegenereerde domeingebeurtenissen opnieuw af te spelen.

In een echte applicatie is dit natuurlijk geen haalbare strategie en we zullen een lokale cache moeten onderhouden om te voorkomen dat de status elke keer wordt gegenereerd. Er zijn andere strategieën, zoals snapshots en roll-up in de gebeurtenisrepository, die het proces kunnen versnellen.

Hiermee is onze poging om event sourcing te introduceren in onze eenvoudige applicatie afgerond.

5.3. Voordelen en nadelen van Event Sourcing

Nu hebben we met succes een alternatieve manier aangenomen om domeinobjecten op te slaan met behulp van event sourcing. Event sourcing is een krachtig patroon en levert veel voordelen op voor een applicatiearchitectuur als het op de juiste manier wordt gebruikt:

  • Maakt schrijf bewerkingen veel sneller aangezien er geen lezen, bijwerken en schrijven vereist is; schrijven is slechts het toevoegen van een gebeurtenis aan een logboek
  • Verwijdert de object-relationele impedantie en dus de behoefte aan complexe mapping tools; Natuurlijk moeten we de objecten nog steeds opnieuw creëren
  • Gebeurt met een controlelogboek verstrekken als bijproduct, die volledig betrouwbaar is; we kunnen precies debuggen hoe de toestand van een domeinmodel is veranderd
  • Het maakt het mogelijk ondersteuning van tijdelijke vragen en het bereiken van tijdreizen (de domeinstatus op een punt in het verleden)!
  • Het is een natuurlijk geschikt voor het ontwerpen van losjes gekoppelde componenten in een microservices-architectuur die asynchroon communiceren door berichten uit te wisselen

Maar zoals altijd is zelfs sourcing van evenementen geen wondermiddel. Het dwingt ons om een ​​drastisch andere manier te kiezen om gegevens op te slaan. Dit kan in verschillende gevallen niet nuttig blijken te zijn:

  • Er is een bijbehorende leercurve en een mentaliteitsverandering vereist om event sourcing toe te passen; het is om te beginnen niet intuïtief
  • Het maakt het nogal moeilijk om typische vragen te behandelen omdat we de staat opnieuw moeten creëren, tenzij we de staat in de lokale cache houden
  • Hoewel het op elk domeinmodel kan worden toegepast, is het geschikter voor het op gebeurtenissen gebaseerde model in een gebeurtenisgestuurde architectuur

6. CQRS met Event Sourcing

Nu we hebben gezien hoe we Event Sourcing en CQRS individueel kunnen introduceren in onze eenvoudige applicatie, is het tijd om ze samen te brengen. Het zou moeten zijn redelijk intuïtief nu deze patronen veel van elkaar kunnen profiteren. We zullen het in deze sectie echter explicieter maken.

Laten we eerst kijken hoe de applicatie-architectuur ze samenbrengt:

Dit zou op dit moment geen verrassing moeten zijn. We hebben de schrijfzijde van de repository vervangen door een event store, terwijl de leeszijde van de repository hetzelfde blijft.

Houd er rekening mee dat dit niet de enige manier is om Event Sourcing en CQRS in de applicatiearchitectuur te gebruiken. Wij kan behoorlijk innovatief zijn en deze patronen samen met andere patronen gebruiken en bedenk verschillende architectuuropties.

Wat hier belangrijk is, is ervoor te zorgen dat we ze gebruiken om de complexiteit te beheersen, niet om de complexiteit alleen maar verder te vergroten!

6.1. CQRS en Event Sourcing samenbrengen

Na Event Sourcing en CQRS afzonderlijk te hebben geïmplementeerd, zou het niet zo moeilijk moeten zijn om te begrijpen hoe we ze samen kunnen brengen.

Goed begin met de applicatie waarin we CQRS hebben geïntroduceerd en breng gewoon relevante wijzigingen aan om event sourcing in de kudde te brengen. We maken ook gebruik van dezelfde evenementen- en evenementenwinkel die we hebben gedefinieerd in onze applicatie waarin we evenementensourcing hebben geïntroduceerd.

Er zijn slechts een paar wijzigingen. We beginnen met het wijzigen van het aggregaat in gebeurtenissen genereren in plaats van de status bij te werken:

openbare klasse UserAggregate {privé EventStore writeRepository; openbare UserAggregate (EventStore-repository) {this.writeRepository = repository; } openbare lijst handleCreateUserCommand (CreateUserCommand commando) {UserCreatedEvent event = nieuwe UserCreatedEvent (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addEvent (command.getUserId (), gebeurtenis); retourneer Arrays.asList (gebeurtenis); } openbare lijst handleUpdateUserCommand (UpdateUserCommand-commando) {User user = UserUtility.recreateUserState (writeRepository, command.getUserId ()); Lijstgebeurtenissen = nieuwe ArrayList (); Lijst contactenToRemove = user.getContacts (). Stream () .filter (c ->! Command.getContacts (). Bevat (c)) .collect (Collectors.toList ()); voor (Contactcontact: contactsToRemove) {UserContactRemovedEvent contactRemovedEvent = nieuwe UserContactRemovedEvent (contact.getType (), contact.getDetail ()); events.add (contactRemovedEvent); writeRepository.addEvent (command.getUserId (), contactRemovedEvent); } Lijst met contactsToAdd = command.getContacts (). Stream () .filter (c ->! User.getContacts (). Bevat (c)) .collect (Collectors.toList ()); voor (Contactcontact: contactsToAdd) {UserContactAddedEvent contactAddedEvent = nieuwe UserContactAddedEvent (contact.getType (), contact.getDetail ()); events.add (contactAddedEvent); writeRepository.addEvent (command.getUserId (), contactAddedEvent); } // verwerk op dezelfde manier adressenToRemove // ​​verwerk op dezelfde manier addressenToAdd retourgebeurtenissen; }}

De enige andere vereiste verandering is in de projector, die nu nodig is procesgebeurtenissen in plaats van domeinobjectstatussen:

openbare klasse UserProjector {UserReadRepository readRepository = nieuwe UserReadRepository (); openbare UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } public void project (String userId, List events) {for (Event event: events) {if (event instanceof UserAddressAddedEvent) apply (userId, (UserAddressAddedEvent) event); if (gebeurtenisinstantie van UserAddressRemovedEvent) apply (userId, (UserAddressRemovedEvent) -gebeurtenis); als (gebeurtenisinstantie van UserContactAddedEvent) van toepassing is (userId, (UserContactAddedEvent) -gebeurtenis); if (gebeurtenisinstantie van UserContactRemovedEvent) toepassen (userId, (UserContactRemovedEvent) -gebeurtenis); }} public void apply (String userId, UserAddressAddedEvent event) {Address address = new Address (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = Optioneel.ofNullable (readRepository.getUserAddress (userId)) .orElse (nieuw UserAddress ()); Stel adressen in = Optioneel.ofNullable (userAddress.getAddressByRegion () .get (address.getState ())) .orElse (nieuwe HashSet ()); adressen.add (adres); userAddress.getAddressByRegion () .put (address.getState (), adressen); readRepository.addUserAddress (userId, userAddress); } public void apply (String userId, UserAddressRemovedEvent event) {Address address = new Address (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = readRepository.getUserAddress (userId); if (userAddress! = null) {Stel adressen in = userAddress.getAddressByRegion () .get (address.getState ()); if (adressen! = null) adressen.remove (adres); readRepository.addUserAddress (userId, userAddress); }} public void apply (String userId, UserContactAddedEvent-gebeurtenis) {// Evenzo omgaan met UserContactAddedEvent-gebeurtenis} public void apply (String userId, UserContactRemovedEvent-gebeurtenis) {// Evenzo omgaan met UserContactRemovedEvent-gebeurtenis}}

Als we ons de problemen herinneren die we hebben besproken tijdens het omgaan met op de staat gebaseerde projectie, is dit een mogelijke oplossing daarvoor.

De op gebeurtenissen gebaseerde projectie is tamelijk handig en gemakkelijker te implementeren. Het enige wat we hoeven te doen is alle voorkomende domeingebeurtenissen verwerken en deze toepassen op alle gelezen domeinmodellen. Typisch, in een op gebeurtenissen gebaseerde toepassing, luistert de projector naar domeingebeurtenissen waarin hij geïnteresseerd is en vertrouwt hij niet op iemand die hem rechtstreeks belt.

Dit is vrijwel alles wat we hoeven te doen om Event Sourcing en CQRS samen te brengen in onze eenvoudige applicatie.

7. Conclusie

In deze tutorial hebben we de basisprincipes van Event Sourcing en CQRS-ontwerppatronen besproken. We ontwikkelden een eenvoudige applicatie en pasten deze patronen er individueel op toe.

Tijdens het proces begrepen we de voordelen die ze met zich meebrachten en de nadelen die ze met zich meebrengen. Ten slotte begrepen we waarom en hoe we beide patronen samen in onze applicatie konden opnemen.

De eenvoudige toepassing die we in deze tutorial hebben besproken, komt niet eens in de buurt van de noodzaak van CQRS en Event Sourcing. Onze focus was om de basisconcepten te begrijpen, daarom was het voorbeeld triviaal. Maar zoals eerder vermeld, kan het voordeel van deze patronen alleen worden gerealiseerd in applicaties met een redelijk complex domeinmodel.

Zoals gewoonlijk is de broncode voor dit artikel te vinden op GitHub.