CQRS és Események beszerzése Java-ban

1. Bemutatkozás

Ebben az oktatóanyagban a Command Query Responsibility Segregation (CQRS) és az Event Sourcing tervezési minták alapfogalmait tárjuk fel.

Noha gyakran kiegészítő mintaként emlegetjük, megpróbáljuk külön megérteni őket, és végül meglátjuk, hogyan egészítik ki egymást. Számos eszköz és keretrendszer létezik, például az Axon, amelyek segítenek ezeknek a mintáknak az átvételében, de az alapok megértéséhez létrehozunk egy egyszerű alkalmazást a Java-ban.

2. Alapfogalmak

Először elméletileg fogjuk megérteni ezeket a mintákat, mielőtt megpróbálnánk megvalósítani őket. Továbbá, mivel ezek az egyes minták elég jól állnak, megpróbáljuk megérteni őket anélkül, hogy kevernénk őket.

Felhívjuk figyelmét, hogy ezeket a mintákat gyakran együtt használják egy vállalati alkalmazásban. Ebben a tekintetben számos más vállalati architektúra mintájuk is profitál. Néhányukat megbeszéljük, miközben megyünk.

2.1. Események beszerzése

Események beszerzése egy új módszert ad az alkalmazásállapot fennmaradásának rendezett eseménysorozatként. Szelektíven lekérdezhetjük ezeket az eseményeket és rekonstruálhatjuk az alkalmazás állapotát az idő bármely pontján. Természetesen ahhoz, hogy ez működjön, újra kell képzelnünk az alkalmazás állapotának minden változását eseményként:

Ezek az események itt olyan tények, amelyek történtek, és amelyeket nem lehet megváltoztatni - más szavakkal, változhatatlannak kell lenniük. Az alkalmazás állapotának helyreállítása csak az összes esemény visszajátszása.

Vegye figyelembe, hogy ez egyúttal lehetőséget kínál az események szelektív visszajátszására, egyes események fordított lejátszására és még sok minden másra. Ennek következtében magát az alkalmazásállapotot másodlagos állampolgárként kezelhetjük, az eseménynaplóval az elsődleges igazságforrásként.

2.2. CQRS

Leegyszerűsítve: a CQRS az az alkalmazás architektúra parancs- és lekérdezési oldalának elkülönítéséről. A CQRS a Command Query Separation (CQS) elvén alapszik, amelyet Bertrand Meyer javasolt. A CQS azt javasolja, hogy a tartományobjektumokkal végzett műveleteket két külön kategóriára osszuk fel: Lekérdezések és Parancsok:

A lekérdezések eredményt adnak, és nem változtatják meg a megfigyelhető állapotot egy rendszer. A parancsok megváltoztatják a rendszer állapotát, de nem feltétlenül adnak vissza értéket.

Ezt úgy érjük el, hogy tisztán elkülönítjük a tartománymodell Parancs és Lekérdezés oldalát. Tovább léphetünk, feloszthatjuk az adattár írási és olvasási oldalát is, természetesen olyan mechanizmus bevezetésével, amely szinkronban tartja őket.

3. Egyszerű alkalmazás

Először leírunk egy egyszerű alkalmazást a Java-ban, amely tartománymodellt épít fel.

Az alkalmazás CRUD műveleteket fog kínálni a tartománymodellen, és a tartományobjektumok számára is kitartást fog tartalmazni. A CRUD jelentése Létrehozás, Olvasás, Frissítés és Törlés, amelyek alapvető műveletek, amelyeket elvégezhetünk domain objektumon.

Ugyanezt az alkalmazást fogjuk használni az Események beszerzése és a CQRS későbbi szakaszokban történő bevezetésére.

Ennek során példánkban felhasználjuk a tartományvezérelt tervezés (DDD) néhány fogalmát.

A DDD a komplex tartományspecifikus tudásra támaszkodó szoftverek elemzésével és tervezésével foglalkozik. Arra az elképzelésre épít, hogy a szoftverrendszereknek egy jól kidolgozott domain modellen kell alapulniuk. A DDD-t először Eric Evans írta fel mintakatalógusként. Néhány példát felhasználunk a példánk megalkotására.

3.1. Az alkalmazás áttekintése

Felhasználói profil létrehozása és kezelése tipikus követelmény sok alkalmazásban. Meghatározunk egy egyszerű tartományi modellt, amely rögzíti a felhasználói profilt és a kitartást:

Mint láthatjuk, a tartományi modellünk normalizált és számos CRUD műveletet tesz lehetővé. Ezek a műveletek csak bemutatás céljából, és a követelményektől függően lehet egyszerű vagy összetett. Sőt, a perzisztencia-tár itt lehet memóriában, vagy használhat helyette adatbázist.

3.2. Alkalmazás megvalósítása

Először Java-osztályokat kell létrehoznunk, amelyek a tartományi modellünket képviselik. Ez meglehetősen egyszerű tartománymodell, és nem is igényelheti a tervezési minták összetettségét mint az Események beszerzése és a CQRS. Mindazonáltal megtartjuk ezt az egyszerűséget, hogy az alapok megértésére összpontosítsunk:

public class Felhasználó {private String userid; privát karakterlánc keresztnév; privát karakterlánc vezetéknév; privát Set kapcsolatok; privát Set címek; // getters and setters} public class Contact {private String type; privát húr részlet; // getters and setters} public class Cím {private String város; privát String állam; privát karakterlánc irányítószám; // szerelők és beállítók}

Ezenkívül meghatározunk egy egyszerű memóriatárat az alkalmazás állapotának fennmaradásához. Természetesen ez nem ad hozzá értéket, de elegendő a későbbi bemutatónkhoz:

public class UserRepository {private Map store = new HashMap (); }

Most meghatározunk egy szolgáltatást, amely a tipikus CRUD műveleteket tárja fel a tartománymodellünkön:

public class UserService {private UserRepository repository; public UserService (UserRepository repository) {this.repository = repository; } public void createUser (karakterlánc userId, karakterlánc keresztnév, karakterlánc vezetéknév) {User user = új felhasználó (userId, keresztnév, vezetéknév); repository.addUser (userId, user); } public void updateUser (String userId, Névjegyek beállítása, Címek beállítása) {User user = adattár.getUser (userId); user.setContacts (kapcsolattartók); user.setAddresses (címek); repository.addUser (userId, user); } public set getContactByType (String userId, String contactType) {Felhasználó user = repository.getUser (userId); Set contacts = user.getContacts (); return contacts.stream () .filter (c -> c.getType (). egyenlő (contactType)) .collect (Collectors.toSet ()); } public set getAddressByRegion (String userId, String állapot) {User user = repository.getUser (userId); Címek beállítása = user.getAddresses (); return address.stream () .filter (a -> a.getState (). egyenlő (állapot)) .collect (Collectors.toSet ()); }}

Nagyjából ezt kell tennünk az egyszerű alkalmazás beállításához. Ez messze nem gyártásra kész kód, de feltár néhány fontos pontot hogy ezt a bemutatót később megvitatjuk.

3.3. Problémák ebben az alkalmazásban

Mielőtt tovább folytatnánk az Események beszerzésével és a CQRS-lel folytatott megbeszélésünket, érdemes megvitatni a jelenlegi megoldás problémáit. Végül is ugyanazokkal a problémákkal fogunk foglalkozni ezen minták alkalmazásával!

Az itt észlelhető sok probléma közül csak kettőre szeretnénk koncentrálni:

  • Domain modell: Az írási és olvasási műveletek ugyanazon tartománymodellen keresztül zajlanak. Bár ez egy ilyen egyszerű tartománymodell számára nem jelent problémát, súlyosbodhat, mivel a tartományi modell összetettebbé válik. Lehetséges, hogy optimalizálnunk kell a tartományi modellünket és a mögöttes tárhelyet, hogy megfeleljenek az olvasási és írási műveletek egyéni igényeinek.
  • Kitartás: A tartományobjektumainkkal szembeni kitartás csak a tartománymodell legújabb állapotát tárolja. Noha ez a legtöbb helyzetben elegendő, egyes feladatokat kihívást jelent. Például, ha előzetes auditot kell végeznünk arról, hogy a tartományi objektum miként változott meg, akkor ez itt nem lehetséges. Ennek eléréséhez ki kell egészítenünk a megoldást néhány auditnaplóval.

4. A CQRS bemutatása

Az utolsó szakaszban tárgyalt első problémával a CQRS mintázat bevezetésével kezdjük az alkalmazásunkat. Ennek részeként különválasztjuk a tartományi modellt és annak kitartását az írási és olvasási műveletek kezeléséhez. Lássuk, hogy a CQRS minta hogyan strukturálja az alkalmazásunkat:

Az itt bemutatott ábra elmagyarázza, hogyan kívánjuk tisztán elkülöníteni az alkalmazás architektúránkat az oldalak írására és olvasására. Itt azonban jó néhány új komponenst vezettünk be, amelyeket jobban meg kell értenünk. Felhívjuk figyelmét, hogy ezek nem kapcsolódnak szigorúan a CQRS-hez, de a CQRS nagy előnyökkel jár:

  • Összesítő / összesítő:

Az összesített a tartományvezérelt tervezésben (DDD) leírt minta, amely logikailag csoportosítja a különböző entitásokat, az entitásokat egy összesített gyökérhez kötve. Az összesített minta tranzakciós konzisztenciát biztosít az entitások között.

A CQRS természetesen profitál az összesített mintából, amely az írási tartomány modelljét csoportosítja, tranzakciós garanciákat biztosítva. Az aggregátumok általában gyorsítótárazott állapotban vannak a jobb teljesítmény érdekében, de anélkül is tökéletesen működhetnek.

  • Vetítés / Vetítő:

A vetítés egy másik fontos minta, amely nagy előnyökkel jár a CQRS számára. Kivetítés lényegében azt jelenti, hogy a doménobjektumokat különböző alakokban és szerkezetekben képviselik.

Az eredeti adatok ezen előrejelzései csak olvashatók és erősen optimalizáltak a fokozott olvasási élmény érdekében. Újra dönthetünk úgy, hogy előrejelzéseket tárolunk a jobb teljesítmény érdekében, de ez nem szükséges.

4.1. Az alkalmazás írási oldalának megvalósítása

Először valósítsuk meg az alkalmazás írási oldalát.

Először meghatározzuk a szükséges parancsokat. A A parancs célja a tartománymodell állapotának mutációja. Hogy sikerül-e vagy sem, attól függ, hogy milyen üzleti szabályokat állítunk be.

Nézzük meg a parancsokat:

public class CreateUserCommand {private String userId; privát karakterlánc keresztnév; privát karakterlánc vezetéknév; } public class UpdateUserCommand {private String userId; privát Set címek; privát Set kapcsolatok; }

Ezek elég egyszerű osztályok, amelyek tartalmazzák azokat az adatokat, amelyeket mutálni kívánunk.

Ezután meghatározunk egy összesítést, amely felelős a parancsok elfogadásáért és kezeléséért. Az összesítettek elfogadhatnak vagy elutasíthatnak egy parancsot:

public class UserAggregate {private UserWriteRepository writeRepository; public UserAggregate (UserWriteRepository repository) {this.writeRepository = adattár; } public User handleCreateUserCommand (CreateUserCommand parancs) {User user = új felhasználó (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addUser (user.getUserid (), user); visszatérő felhasználó; } public Felhasználó handUpdateUserCommand (UpdateUserCommand parancs) {Felhasználói felhasználó = writeRepository.getUser (command.getUserId ()); user.setAddresses (command.getAddresses ()); user.setContacts (command.getContacts ()); writeRepository.addUser (user.getUserid (), user); visszatérő felhasználó; }}

Az összesítő egy adattárat használ az aktuális állapot lekérésére és az abban bekövetkező változások fenntartására. Ezenkívül helyileg is tárolhatja az aktuális állapotot, hogy elkerülje az oda-vissza költségeket egy lerakatba, miközben minden parancsot feldolgoz.

Végül egy tárolóra van szükségünk a tartománymodell állapotának megtartásához. Ez általában adatbázis vagy más tartós áruház lesz, de itt egyszerűen egy memóriában lévő adatszerkezettel cseréljük ki őket:

public class UserWriteRepository {private Map store = new HashMap (); // hozzáférők és mutátorok}

Ezzel lezárul alkalmazásunk írási oldala.

4.2. Az alkalmazás olvasási oldalának megvalósítása

Most térjünk át az alkalmazás olvasási oldalára. Először meghatározzuk a domain modell olvasási oldalát:

public class UserAddress {privát térkép addressByRegion = new HashMap (); } public class UserContact {privát térkép contactByType = new HashMap (); }

Ha felidézzük olvasási műveleteinket, nem nehéz belátni, hogy ezek az osztályok tökéletesen feltérképezik őket. Ez a szépség egy olyan domainmodell létrehozásában, amely a lekérdezések köré összpontosul.

Ezután meghatározzuk az olvasási tárat. Ismét csak egy memóriában lévő adatszerkezetet fogunk használni, annak ellenére, hogy ez a valós alkalmazásoknál tartósabb adattároló lesz:

public class UserReadRepository {private Map userAddress = new HashMap (); privát térkép userContact = új HashMap (); // hozzáférők és mutátorok}

Most meghatározzuk a szükséges lekérdezéseket, amelyeket támogatnunk kell. A lekérdezés célja adatszerzés - nem feltétlenül eredményez adatokat.

Nézzük meg kérdéseinket:

public class ContactByTypeQuery {private String userId; privát karakterlánc contactType; } public class AddressByRegionQuery {private String userId; privát String állam; }

Ezek ismét egyszerű Java osztályok, amelyek az adatokat tartalmazzák a lekérdezés meghatározásához.

Amire most szükségünk van, az a vetület, amely képes kezelni ezeket a kérdéseket:

public class UserProjection {private UserReadRepository readRepository; public UserProjection (UserReadRepository readRepository) {this.readRepository = readRepository; } public Set hand (ContactByTypeQuery lekérdezés) {UserContact userContact = readRepository.getUserContact (query.getUserId ()); return userContact.getContactByType () .get (query.getContactType ()); } public Set hand (AddressByRegionQuery lekérdezés) {UserAddress userAddress = readRepository.getUserAddress (query.getUserId ()); return userAddress.getAddressByRegion () .get (query.getState ()); }}

A vetítés itt az általunk korábban definiált olvasási adattárat használja a lekérdezéseink megválaszolására. Ez nagyjából befejezi alkalmazásunk olvasott oldalát is.

4.3. Olvasási és írási adatok szinkronizálása

Ennek a rejtvénynek egy darabja még mindig megoldatlan: nincs mit tenni szinkronizálja írási és olvasási adattárainkat.

Itt lesz szükségünk valamire, mint projektorra. A A projektor rendelkezik azzal a logikával, hogy az írási tartomány modelljét az olvasott tartomány modelljébe vetíti.

Sokkal kifinomultabb módszerek vannak ennek kezelésére, de viszonylag egyszerűen megtartjuk:

public class UserProjector {UserReadRepository readRepository = new UserReadRepository (); public UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } public void project (Felhasználó felhasználó) {UserContact userContact = Opcionális.ofNullable (readRepository.getUserContact (user.getUserid ())) .vagyElse (új UserContact ()); Térkép contactByType = new HashMap (); for (Kapcsolattartó névjegy: user.getContacts ()) {Kapcsolatok beállítása = Optional.ofNullable (contactByType.get (contact.getType ())) .orElse (új HashSet ()); kapcsolatok.add (kapcsolat); contactByType.put (contact.getType (), kapcsolatok); } userContact.setContactByType (contactByType); readRepository.addUserContact (user.getUserid (), userContact); UserAddress userAddress = Optional.ofNullable (readRepository.getUserAddress (user.getUserid ())) .orElse (új UserAddress ()); Térkép addressByRegion = new HashMap (); for (Cím címe: user.getAddresses ()) {Címek beállítása = Optional.ofNullable (addressByRegion.get (address.getState ())) .orElse (új HashSet ()); címek.add (cím); addressByRegion.put (address.getState (), címek); } userAddress.setAddressByRegion (addressByRegion); readRepository.addUserAddress (user.getUserid (), userAddress); }}

Ez inkább ennek nagyon durva módja, de elegendő betekintést nyújt a szükségesekbe hogy a CQRS működjön. Sőt, nem szükséges, hogy az olvasási és írási adattárak különböző fizikai üzletekben üljenek. Az elosztott rendszernek megvan a maga problémája!

Felhívjuk figyelmét, hogy az nem kényelmes az írási tartomány jelenlegi állapotát különböző olvasási tartományú modellekre vetíteni. Az itt bemutatott példa meglehetősen egyszerű, ezért nem látjuk a problémát.

Amint azonban az írási és olvasási modellek bonyolultabbá válnak, egyre nehezebb lesz kivetíteni. Ezt állapotalapú vetítés helyett eseményalapú vetítéssel tudjuk megoldani Események beszerzésével. A megvalósításról később olvashatunk az oktatóanyagban.

4.4. A CQRS előnyei és hátrányai

Megbeszéltük a CQRS mintát, és megtanultuk, hogyan kell bevezetni egy tipikus alkalmazásban. Kategorikusan megpróbáltuk megoldani a tartományi modell merevségével kapcsolatos problémát mind az olvasás, mind az írás kezelése során.

Beszéljünk néhány további előnyről, amelyet a CQRS nyújt az alkalmazás architektúrájának:

  • A CQRS biztosítja számunkra kényelmes módszer különálló tartománymodellek kiválasztására alkalmas írási és olvasási műveletekhez; nem kell komplex tartománymodellt létrehoznunk, mindkettőt támogatva
  • Ez segít nekünk abban válasszon egyénileg megfelelő tárakat az írási és olvasási műveletek bonyolultságának kezelésére, mint például az írás nagy teljesítménye és az olvasás alacsony késleltetése
  • Természetesen kiegészíti az eseményalapú programozási modelleket elosztott architektúrában az aggályok és az egyszerűbb tartománymodellek elkülönítésével

Ez azonban nem jön ingyen. Amint ez az egyszerű példából kitűnik, a CQRS jelentősen összetettebbé teszi az architektúrát. Lehet, hogy nem megfelelő vagy megéri a fájdalmat sok esetben:

  • Csak egy komplex tartománymodell előnyös lehet e minta további összetettségéből; egy egyszerű tartománymodell mindezek nélkül kezelhető
  • Természetesen kódmásoláshoz vezet bizonyos mértékig elfogadható gonoszság ahhoz a nyereséghez képest, amelyhez vezet; azonban egyéni megítélés javasolt
  • Külön tárolók következetességi problémákhoz vezetnek, és nehéz az írási és olvasási adattárakat mindig tökéletes szinkronban tartani; gyakran meg kell elégednünk az esetleges következetességgel

5. Bemutatjuk az események beszerzését

Ezután az egyszerű alkalmazásunkban tárgyalt második problémával foglalkozunk. Ha felidézzük, ez összefüggésbe került a perzisztencia-tárunkkal.

A probléma megoldása érdekében bemutatjuk az Események beszerzését. Események beszerzése drámai módon megváltoztatja az alkalmazásállapot-tárolás gondolkodásmódját.

Lássuk, hogyan változtatja meg az adattárunkat:

Itt strukturáltuk tárházunk a domain események rendezett listájának tárolására. A tartományobjektum minden módosítása eseménynek számít. Hogy egy eseménynek durva vagy finom szemcsésnek kell lennie, az a domain tervezésének kérdése. A fontos szempontokat itt figyelembe kell venni az eseményeknek időbeli rendje van és megváltoztathatatlan.

5.1. Események és eseménybolt megvalósítása

Az eseményvezérelt alkalmazások alapvető objektumai az események, és az események beszerzése nem különbözik egymástól. Ahogy korábban láthattuk, az események a tartománymodell állapotának egy adott időpontban bekövetkező változását jelentik. Tehát kezdjük azzal, hogy meghatározzuk egyszerű alkalmazásunk alapeseményét:

public abstract class Esemény {public final UUID id = UUID.randomUUID (); nyilvános végleges létrehozás dátuma = új dátum (); }

Ez csak azt biztosítja, hogy minden alkalmazásunk által generált esemény egyedi azonosítást és a létrehozás időbélyegét kapja. Ezek szükségesek a további feldolgozáshoz.

Természetesen számos más attribútum is érdekelhet minket, például egy esemény eredetének megállapítására szolgáló attribútum.

Ezután hozzunk létre néhány tartományspecifikus eseményt, amely öröklődik erről az alapeseményről:

a public class UserCreatedEvent kiterjeszti a (z) {private String userId; eseményt; privát karakterlánc keresztnév; privát karakterlánc vezetéknév; } public class UserContactAddedEvent kiterjeszti a (z) {private String contactType; privát String contactDetails; } public class UserContactRemovedEvent kiterjeszti a {private String contactType; privát String contactDetails; } public class UserAddressAddedEvent kiterjeszti a {private String város; privát String állam; privát karakterlánc postCode; } public class UserAddressRemovedEvent kiterjeszti a {private String város; privát String állam; privát karakterlánc postCode; }

Ezek egyszerű Java-POJO-k, amelyek a tartományi esemény részleteit tartalmazzák. Azonban itt fontos megjegyezni az események részletességét.

Hozhattunk volna létre egyetlen eseményt a felhasználói frissítésekhez, de ehelyett úgy döntöttünk, hogy külön eseményeket hozunk létre a cím és a névjegy hozzáadásához és eltávolításához. A választás megfeleltethető annak, ami hatékonyabbá teszi a tartományi modellel való munkát.

Természetesen szükségünk van egy tárhelyre a domain-eseményeink megtartásához:

nyilvános osztály EventStore {privát térkép store = new HashMap (); }

Ez egy egyszerű memória-adatstruktúra a tartományi eseményeink megtartására. A valóságban, számos olyan megoldás létezik, amelyeket kifejezetten az eseményadatok kezelésére hoztak létre, mint például az Apache Druid. Számos olyan általános célú elosztott adattároló létezik, amely képes kezelni az események beszerzését, beleértve a Kafka és a Cassandra termékeket.

5.2. Események generálása és fogyasztása

Tehát most megváltozik az összes CRUD műveletet kezelő szolgáltatásunk. Most a mozgó tartomány állapotának frissítése helyett a tartományi eseményeket is hozzáfűzi. Ugyanazt a tartományi eseményt fogja használni a kérdések megválaszolására.

Lássuk, hogyan érhetjük el ezt:

public class UserService {private EventStore adattár; public UserService (EventStore adattár) {this.repository = repository; } public void createUser (String userId, String firstName, String lastName) {repository.addEvent (userId, new UserCreatedEvent (userId, keresztnév, vezetéknév)); } public void updateUser (String userId, Névjegyek beállítása, Címek beállítása) {User user = UserUtility.recreateUserState (repository, userId); user.getContacts (). stream () .filter (c ->! contacts.contains (c)) .forEach (c -> repository.addEvent (userId, new UserContactRemovedEvent (c.getType (), c.getDetail ()) )); contacts.stream () .filter (c ->! user.getContacts (). tartalmazza (c)) .forEach (c -> repository.addEvent (userId, new UserContactAddedEvent (c.getType (), c.getDetail ()) )); user.getAddresses (). stream () .filter (a ->! address.contains (a)) .forEach (a -> repository.addEvent (userId, new UserAddressRemovedEvent (a.getCity (), a.getState (), a.get irányítószám ())))); address.stream () .filter (a ->! user.getAddresses (). tartalmazza (a)) .forEach (a -> repository.addEvent (userId, new UserAddressAddedEvent (a.getCity (), a.getState (), a.get irányítószám ())))); } public set getContactByType (String userId, String contactType) {User user = UserUtility.recreateUserState (repository, userId); return user.getContacts (). stream () .filter (c -> c.getType (). egyenlő (contactType)) .collect (Collectors.toSet ()); } public set getAddressByRegion (String userId, String állapot) dobja a {User user = UserUtility.recreateUserState (repository, userId) kivételt; return user.getAddresses (). stream () .filter (a -> a.getState (). egyenlő (állapot)) .collect (Collectors.toSet ()); }}

Felhívjuk figyelmét, hogy a felhasználói frissítés itt történő kezelésének részeként több eseményt generálunk. Érdekes megjegyezni, hogy vagyunk a tartománymodell aktuális állapotának előállítása az összes eddig generált tartományi esemény visszajátszásával.

Természetesen egy valódi alkalmazásban ez nem kivitelezhető stratégia, és fenntartanunk kell egy helyi gyorsítótárat, hogy elkerüljük az állam minden alkalommal történő létrehozását. Vannak más stratégiák, mint a pillanatképek és az eseménytárban történő összesítés, amelyek felgyorsíthatják a folyamatot.

Ezzel befejezzük az események beszerzésének egyszerű alkalmazásunkban történő bevezetésére tett erőfeszítéseinket.

5.3. Az események beszerzésének előnyei és hátrányai

Most sikeresen elfogadtuk a domain objektumok tárolásának alternatív módját eseményforrás használatával. Az eseményforrás erőteljes minta, és sok haszonnal jár az alkalmazás architektúrájában, ha megfelelően használják:

  • Teszi sokkal gyorsabban írhat műveleteket mivel nincs szükség olvasásra, frissítésre és írásra; az write csupán egy esemény naplózása
  • Eltávolítja az objektum-relációs impedanciát és ennélfogva komplex térképészeti eszközök igénye; természetesen még mindig vissza kell állítanunk a tárgyakat
  • Történik melléktermékként szolgáltasson ellenőrzési naplót, ami teljesen megbízható; pontosan ki tudjuk deríteni, hogyan változott egy tartománymodell állapota
  • Lehetővé teszi az időbeli lekérdezések támogatása és az időutazás elérése (a domain állapota a múlt egy pontján)!
  • Ez természetes alkalmas lazán összekapcsolt alkatrészek tervezésére mikroszolgáltatások architektúrájában, amelyek aszinkron módon kommunikálnak üzenetváltással

Azonban, mint mindig, még az események beszerzése sem ezüst golyó. Kényszeríti, hogy az adatok tárolásának drámai módon eltérő módját alkalmazzuk. Ez sok esetben nem bizonyulhat hasznosnak:

  • Van társított tanulási görbe és gondolkodásmód-váltás szükséges rendezvények beszerzésének elfogadása; kezdetben nem intuitív
  • Ez teszi meglehetősen nehéz kezelni a tipikus kérdéseket mivel vissza kell állítanunk az állapotot, hacsak nem tartjuk az állapotot a helyi gyorsítótárban
  • Bár bármilyen tartományi modellre alkalmazható, mégis jobban megfelel az eseményalapú modellnek eseményvezérelt architektúrában

6. CQRS Események beszerzésével

Most, hogy láttuk, hogyan lehet az Alkalmazás-beszerzést és a CQRS-t egyenként bevezetni egyszerű alkalmazásunkba, itt az ideje, hogy összehozzuk őket. Kellene lennie meglehetősen intuitív, mivel ezek a minták nagy hasznot húzhatnak egymásból. Ebben a szakaszban azonban világosabbá tesszük.

Először nézzük meg, hogy az alkalmazás architektúra hogyan hozza őket össze:

Ez mára nem lehet meglepetés. A lerakat írási oldalát eseménytárolóvá cseréltük, míg a lerakat olvasási oldala továbbra is ugyanaz.

Felhívjuk figyelmét, hogy ez nem az egyetlen módja az Eseményforrás és a CQRS használatának az alkalmazás architektúrájában. Mi meglehetősen innovatív lehet, és ezeket a mintákat más mintákkal együtt is felhasználhatja és számos építészeti lehetőséggel áll elő.

Ami itt fontos, annak biztosítása, hogy a bonyolultság kezeléséhez felhasználjuk őket, és ne csak egyszerűen tovább növeljük a bonyolultságot!

6.1. A CQRS és az események beszerzése együtt

Miután az Események beszerzését és a CQRS-t külön-külön hajtottuk végre, nem lehet olyan nehéz megérteni, hogyan tudjuk összehozni őket.

Jól azzal az alkalmazással kezdődik, ahol bevezettük a CQRS-t, és csak a szükséges változtatásokat hajtjuk végre hogy az események beszerzése a körbe kerüljön. Kihasználjuk azokat az eseményeket és eseménytárakat is, amelyeket az alkalmazásunkban definiáltunk, ahol bevezettük az események beszerzését.

Csak néhány változás van. Kezdjük azzal, hogy az összesítést megváltoztatjuk események generálása az állapot frissítése helyett:

public class UserAggregate {private EventStore writeRepository; public UserAggregate (EventStore repository) {this.writeRepository = adattár; } public List handleCreateUserCommand (CreateUserCommand parancs) {UserCreatedEvent event = new UserCreatedEvent (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addEvent (command.getUserId (), esemény); return Arrays.asList (esemény); } public List handleUpdateUserCommand (UpdateUserCommand parancs) {User user = UserUtility.recreateUserState (writeRepository, command.getUserId ()); List események = new ArrayList (); Kapcsolattartók listájaToRemove = user.getContacts (). Stream () .filter (c ->! Command.getContacts (). Tartalmazza (c)) .collect (Collectors.toList ()); a (Contact contact: contactsToRemove) {UserContactRemovedEvent contactRemovedEvent = new UserContactRemovedEvent (contact.getType (), contact.getDetail ()); events.add (contactRemovedEvent); writeRepository.addEvent (command.getUserId (), contactRemovedEvent); } List contactsToAdd = command.getContacts (). Stream () .filter (c ->! User.getContacts (). Tartalmazza (c)) .collect (Collectors.toList ()); for (Kapcsolattartó névjegy: contactsToAdd) {UserContactAddedEvent contactAddedEvent = new UserContactAddedEvent (contact.getType (), contact.getDetail ()); events.add (contactAddedEvent); writeRepository.addEvent (command.getUserId (), contactAddedEvent); } // hasonlóan dolgozza fel a címeketToRemove // ​​hasonló módon feldolgozza a címeket }}

Az egyetlen további szükséges változtatás a projektorban van, amire most szükség van eseményeket dolgozzon fel tartományobjektum-állapotok helyett:

public class UserProjector {UserReadRepository readRepository = new UserReadRepository (); public UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } public void project (String userId, List események) {for (Esemény esemény: események) {for (Esemény (UserAddressAddedEvent) esemény érvényes (userId, (UserAddressAddedEvent) esemény); if (a UserAddressRemovedEvent eseményeseménye) érvényes (userId, (UserAddressRemovedEvent) esemény); if (a UserContactAddedEvent eseményeseménye) érvényes (userId, (UserContactAddedEvent) esemény); if (a UserContactRemovedEvent eseményeseménye) alkalmazható (userId, [UserContactRemovedEvent) esemény); }} public void Apply (String userId, UserAddressAddedEvent esemény) {Address address = új cím (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = Opcionális.Nullable (readRepository.getUserAddress (userId)) .vagyElse (új UserAddress ()); Címek beállítása = Optional.ofNullable (userAddress.getAddressByRegion () .get (address.getState ())) .orElse (új HashSet ()); címek.add (cím); userAddress.getAddressByRegion () .put (address.getState (), címek); readRepository.addUserAddress (userId, userAddress); } public void Apply (karakterlánc userId, UserAddressRemovedEvent esemény) {Address address = új cím (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = readRepository.getUserAddress (userId); if (userAddress! = null) {Címek beállítása = userAddress.getAddressByRegion () .get (address.getState ()); if (címek! = null) címek.eltávolítás (cím); readRepository.addUserAddress (userId, userAddress); }} public void Apply (String userId, UserContactAddedEvent esemény) {// Hasonlóképpen kezelje a UserContactAddedEvent esemény} public void Apply (String userId, UserContactRemovedEvent esemény) {// Hasonlóképpen kezelje a UserContactRemovedEvent eseményt}

Ha felidézzük azokat a problémákat, amelyeket az állapotalapú vetítés kezelése során tárgyaltunk, ez egy lehetséges megoldás erre.

A az eseményalapú vetítés meglehetősen kényelmes és könnyebben kivitelezhető. Csak annyit kell tennünk, hogy feldolgozzuk az összes előforduló tartományi eseményt, és alkalmazzuk azokat az összes olvasott tartománymodellre. Általában egy eseményalapú alkalmazásban a kivetítő meghallgatja az őt érdeklő tartományi eseményeket, és nem hagyatkozik abban, hogy valaki közvetlenül hívja.

Nagyjából ennyit kell tennünk, hogy összehozzuk az Eseményforrás és a CQRS-t egyszerű alkalmazásunkban.

7. Következtetés

Ebben az oktatóanyagban megvitattuk az Események beszerzése és a CQRS tervezési mintázatának alapjait. Kidolgoztunk egy egyszerű alkalmazást, és ezeket a mintákat egyenként alkalmaztuk rá.

Ennek során megértettük az általuk nyújtott előnyöket és a hátrányokat. Végül megértettük, miért és hogyan építsük be ezeket a mintákat együtt alkalmazásunkba.

Az ebben az oktatóanyagban tárgyalt egyszerű alkalmazás közel sem igazolja a CQRS és az Események beszerzésének szükségességét. Célunk az alapfogalmak megértése volt, ezért a példa triviális volt. De mint korábban említettük, ezeknek a mintáknak az előnye csak azokban az alkalmazásokban valósulhat meg, amelyek ésszerűen összetett tartománymodellel rendelkeznek.

Szokás szerint a cikk forráskódja megtalálható a GitHubon.