Event Sourcing umsetzen Was? Wie?

Detlef7able

Aktives Mitglied
Guten Tag,

in einer Übungsaufgabe soll ich Event Sourcing in Java umsetzen.
Mir ist die Sache sehr unverständlich. Ich weiß nicht was es bringt, warum man es einsetzt. Ein Beispiel zu der Thematik liegt mir vor, verstehe das aber auch eher schlecht.

Ich habe eine Klasse Teilnehmer mit Name und Wohnort. Die Teilnehmer untergliedern sich dann spezieller in Vortragender, Zuhörer und Helfer.

Für diese Teilnehmer sollen nun Zustände angelegt werden:

Jeder Teilnehmer soll einen bestimmten Status hinsichtlich seiner Anmeldung haben. Es können dadurch drei verschiedene Zustände erreicht werden: ANGEMELDET, ANGEMELDETBEZAHLT und ABGEMELDET.

Jeder Wechsel des Zustandes führt zu einem Ereignis. Zu Beginn befindet sich der Teilnehmer im Zustand ANGEMELDET. Wenn das Ereignis "Zahlung" auftritt, dann wird der Teilnehmer in den Zustand ANGEMELDETBEZAHLT gesetzt. Wenn danach das Ereignis "Abmeldung" auftritt, dann wird der Teilnehmer in den Zustand abgemeldet überführt.

Im Beispiel das mir vorliegt gibt es ein DomainEvent von dem alle anderen Events erben und einen EventProcessor der die Events verarbeitet.
 
Was ist jetzt die Frage?

Generell werden beim Event Sourcing Änderungen am Zustand durch einzelne Ereignisse (Events) aufgezeichnet. Ausgehend von einem Grundzustand kann so der aktuelle Zustand ermittelt werden oder auch zu einem früheren Zustand zurück gegangen werden. Mal ganz grob erklärt.

Ein großer Vorteil ergibt sich da für die Nachvollziehbarkeit, denn es kann jederzeit nachvollzogen werden, wie der aktuelle Zustand zustande gekommen ist. Das kann für rechtliche Themen sinnvoll sein. Hier im Forum wurde mal Arbeitszeiterfassung diskutiert. Das würde sich meiner Ansicht nach gut für Event Sourcing eignen.
 
Zuletzt bearbeitet:
Ich weiß nicht wie der generelle Aufbau von EventSourcing ist.

Wenn ich da jetzt erste Versuche unternehme fühle ich mich komplett verloren. Ich weiß ja gar nicht ob das korrekt ist.
 
Vielleicht hilft dieses?

Sorry, es ist sinnlos jetzt alles noch einmal zu schreiben, was man sowieso im Netz dazu finden kann.

Falls konkretere Fragen auftauchen kannst du die natürlich gerne stellen.
 
So also ich habe einen EventProcessor der die Ereignisse aufzeichnet und ein DomainEvent welches als Grundlage für die Events dient.

Bei den einzelnen Events muss der Zustand gesetzt werden und ein Zeitstempel wird dokumentiert. Kommt sonst noch etwas hinzu?
 
Was muss hier noch hinzugefügt werden?

Java:
package veranstaltung;

import java.time.LocalDate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class DomainEvent {
  
    private static final Logger log = LoggerFactory.getLogger(DomainEvent.class);
  
    LocalDate recordedDate, occuredDate;
  
    public DomainEvent(LocalDate occuredDate) {
        log.info("created at "+occuredDate.toString());
        this.occuredDate = occuredDate;
        this.recordedDate=LocalDate.now();
    }

    public abstract void process();
}

Java:
package veranstaltung;

import java.time.LocalDate;
import veranstaltung.Teilnehmer.Status;

public class PayedEvent extends DomainEvent{
  
    public PayedEvent (LocalDate date, Teilnehmer t)
    {
        super (date);
        t.setSt(Status.ANGEMELDETBEZAHLT);
    }

    @Override
    public void process() {
        // TODO Auto-generated method stub
      
    }
}

[CODE=java]package veranstaltung;

import veranstaltung.DomainEvent;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EventProcessor {

    private static final Logger log= LoggerFactory.getLogger(EventProcessor.class);
   
    List logList= new ArrayList<DomainEvent>();
   
    public void process(DomainEvent domainEvent)
    {
        log.info("process"+domainEvent.getClass());
        domainEvent.process();
        logList.add(domainEvent);
    }
   
}
}
Java:
    package veranstaltung;

    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;

    @Entity
    public class Teilnehmer {

  
    static int idnumber=0;
  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String vorname;
    private String wohnort;
    public enum Status {ANGEMELDET, ANGEMELDETBEZAHLT, ABGEMELDET}
    private Status st;

    protected Teilnehmer() {}
  
    public Teilnehmer(Long id, String name, String vorname,String wohnort, Status st)
    {
        this.id=id;
        this.name=name;
        this.vorname=vorname;
        this.wohnort=wohnort;
        st=st.ANGEMELDET;
    }
  
  
    public static String erzeugeID ()
    {
        String id= "JAVALAND-";
        id=id+idnumber;
        idnumber++;
        return id;
    }
  
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getVorname() {
        return vorname;
    }

    public void setVorname(String vorname) {
        this.vorname = vorname;
    }

    public String getWohnort() {
        return wohnort;
    }

    public void setWohnort(String wohnort) {
        this.wohnort = wohnort;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
  
  
    public Status getSt() {
        return st;
    }

    public void setSt(Status st) {
        this.st = st;
    }
}
 
Das ist aus meiner Sicht schlicht falsch.

Du hast den Sinn von den Events ganz offensichtlich nicht verstanden.

An einer Stelle entstehen Events und an anderer Stelle findet die Verarbeitung statt.

Damit ist sowas:
Java:
    public PayedEvent (LocalDate date, Teilnehmer t)
    {
        super (date);
        t.setSt(Status.ANGEMELDETBEZAHLT);
    }
einfach nur Müll.

Man sollte sich auch überlegen, ob process() wirklich in das Event gehören. Das hebelt ja die Trennung wieder aus. Bei der Erstellung des Events "Teilnehmer hat bezahlt" ist ja unbekannt, was das für Konsequenzen hat. Diese Logik gehört auch nicht in das Event. Das Event beschreibt nur einen Sachverhalt und nicht das, was daraus resultiert.

Was alles passiert, wenn ein Hund Dich beisst, weisst Du nicht. Und das Event "Ein Hund hat Dich gebissen" hat absolut keine Ahnung was dann passiert. Das kann sich auch ständig ändern. Das geht an die Polizei und die prüft da irgendwas. Evtl. war das ein Polizeihund, der Dich gebissen hat, denn Du hattest ein Messer in der Hand und hast damit jemanden bedroht. Dann würde der Hund gelobt und Du bekommst eine Latte Anzeigen. Das kann aber auch ein privater Hund gewesen sein... Dann wird ggf. eine Anzeige gegen den Halter erstellt, der Tierschutz wird informiert, das Wesen des Hundes wird geprüft, .....
Das wird also komplex und das gehört nicht in das Event. Das Event ist nur ein Zettel mit notwendigen Informationen, der weiter gegeben wird.

Hier ist aber evtl. das Lehrmaterial zu prüfen - nicht dass es bei euch exakt so gemacht werden soll.
 
Ok, Fowler hat dem Event auch ein process gegeben. Der Part ist dann wohl so gewollt.

Ändert aber nichts an dem ersten Punkt den ich gebracht habe.
 
Ach ja - Fowler geht da auch im Detail drauf ein - dem Link sollte man also durchaus auch folgen und das lesen. Hier geht es speziell um den Abschnitt "Structuring the Event Handler Logic"
 
Im Eventkonstruktor wird ein neues Objekt erstellt, dabei wird der neue Status entsprechend gesetzt, da es kein update oder löschen geben darf im ES.

Ist dies korrekt?

Was passiert in der process-Methode? Brauch ich die für meinen Fall überhaupt?
 
Na ja bspw. im ArrivalEvent wird Hafen und Schiff auf die Eingangsparameter des Konstruktors gesetzt.

In der process-Methode wird die handleArrival-Methode aufgerufen. Da wird der Hafen des Schiffes auf den gesetzt, welcher der Hafen des ArrivalEvents ist.

Das was danach kommt kann ich nicht nachvollziehen sowas wie cargo -> cargo..... hab ich noch nie gesehen.
 
Na ja bspw. im ArrivalEvent wird Hafen und Schiff auf die Eingangsparameter des Konstruktors gesetzt.
Dann solltest Du auch sowas machen und halt die Parameter schlicht und einfach erst einmal im Event speichern
In der process-Methode wird die handleArrival-Methode aufgerufen. Da wird der Hafen des Schiffes auf den gesetzt, welcher der Hafen des ArrivalEvents ist.
Da kommt dann das hin, was auf Grund des Events passieren soll. Also das, was Du bereits in den Konstruktor gebaut hast.
 
Würde das den Prinzipien entsprechen?
Java:
package veranstaltung;

import java.time.LocalDate;
import veranstaltung.Teilnehmer.Status;

public class PayedEvent extends DomainEvent{
    
    
    private Teilnehmer teilnehmer;
    
    public PayedEvent (LocalDate date, Teilnehmer t)
    {
        super (date);
        setTeilnehmer(teilnehmer);
    }

    @Override
    public void process() {
    this.teilnehmer.setSt(Status.ANGEMELDETBEZAHLT);
        
    }
    
    public PayedEvent setTeilnehmer(Teilnehmer teilnehmer)
    {
        this.teilnehmer=teilnehmer;
        return this;
    }
}
 
Würde das den Prinzipien entsprechen?
Vom o.g. Schiff-Beispiel übertragen, müsste es ungefähr so aussehen:
Java:
public class PayedEvent extends DomainEvent{

    private Teilnehmer teilnehmer;
 
    public PayedEvent (LocalDate date, Teilnehmer teilnehmer)
    {
        super (date);
        this.teilnehmer = teilnehmer;
    }

    @Override
    public void process() {
        teilnehmer.handlePayedEvent(this);
    }

    // ein getter für "teilnehmer" ist hier evtl. sinnvoll, um später darauf zugreifen zu können
}

Die Verarbeitung des Events liegt beim Teilnehmer. Die Klasse Teilnehmer muss eine Methode handlePayedEvent() implementieren und ihren Zustand entsprechend ändern (abhängig von der darin enthaltenen Logik). Das Event, weiß nicht, was der korrekte Status des Teilnehmers sein muss, woher auch?
 
Zuletzt bearbeitet:
Guten Tag!
Ich wollte das Beispielprojekt laufen lassen.
Der import
Java:
import org.springframework.boot.test.context.SpringBootTest;
wird nicht erkannt wie ist dieser einzufügen?
 
Wenn Du Spring Boot nutzen willst, dann solltest Du auch ein entsprechendes Projekt haben. Woher kommt denn Dein Projekt, das Du verwendest? Was für ein Projekt ist es?

Bei Spring Boot ist es ja üblich, dieses per Spring Initializr zu erstellen. Da sind dann aber die Abhängigkeiten für die Tests mit drin.

In Maven wäre es also eine Abhängigkeit a.la.:
Java:
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <version>${spring.boot.version}</version>
      <scope>test</scope>
    </dependency>
(Wobei das version-tag beim initializr fehlt, da die Version im Parent POM vorgegeben wird und daher nicht mehr spezifiziert werden muss.)

Und da es eine Test-Abhängigkeit ist, darf das natürlich auch nur in den Test Sources verwendet werden ...
 
Ich habe das Programm bereits vor ein paar Tagen ausgeführt. Nun zeigt es mir hier diese Fehlermeldung an.

Ich kopiere die POM mal hier rein:

Code:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.htwdresden.informatik</groupId>
    <artifactId>eventSourcingSimpleDemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eventSourcingSimpleDemo</name>
    <description>Simple Demo for Event Sourcing</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
 
Dann ist die Abhängigkeit im Test Bereich vorhanden. In welcher Klasse hast Du den Import und wo liegt diese Datei genau?
 
Die Klasse ist eine Testklasse für das EventSourcing.

Sie liegt im package eventSourcingSimpleDemo.
Im package eventSourcingSimpleDemo liegen die Packages event, model und service.

Java:
package org.htwdresden.informatik.eventSourcingSimpleDemo;

import org.htwdresden.informatik.eventSourcingSimpleDemo.event.ArrivalEvent;
import org.htwdresden.informatik.eventSourcingSimpleDemo.event.DepartureEvent;
import org.htwdresden.informatik.eventSourcingSimpleDemo.event.LoadEvent;
import org.htwdresden.informatik.eventSourcingSimpleDemo.event.UnloadEvent;
import org.htwdresden.informatik.eventSourcingSimpleDemo.model.Cargo;
import org.htwdresden.informatik.eventSourcingSimpleDemo.model.Country;
import org.htwdresden.informatik.eventSourcingSimpleDemo.model.Port;
import org.htwdresden.informatik.eventSourcingSimpleDemo.model.Ship;
import org.htwdresden.informatik.eventSourcingSimpleDemo.service.EventProcessor;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;

import java.time.LocalDate;
import java.time.Month;

@SpringBootTest
class EventSourcingSimpleDemoApplicationTests {

    private static final Logger log = LoggerFactory.getLogger(EventSourcingSimpleDemoApplicationTests.class);

    Ship kingOtto;
    Port hamburg, rotterdam, rostock, london;
    Cargo wheat;
    EventProcessor eventProcessor;

    @Test
    void arrivalSetsShipsLocation() {
        testSetup();

        ArrivalEvent arrivalEvent = new ArrivalEvent(LocalDate.of(2020, Month.APRIL, 1), rostock, kingOtto);
        eventProcessor.process(arrivalEvent);
        Assert.isTrue(kingOtto.getPort()==rostock, "port must be rostock");
    }

    @Test
    void departurePutsShipOutToSea() {
        testSetup();

        eventProcessor.process(new ArrivalEvent(LocalDate.of(2020, 2, 10), hamburg, kingOtto));
        eventProcessor.process(new ArrivalEvent(LocalDate.of(2020, 3, 3), rotterdam, kingOtto));
        eventProcessor.process(new DepartureEvent(LocalDate.of(2020, 3, 4), rotterdam, kingOtto));

        Assert.isTrue(Port.AT_SEA == kingOtto.getPort(), "port must be AT_SEA");
//.............
    }
 
Und da es eine Test-Abhängigkeit ist, darf das natürlich auch nur in den Test Sources verwendet werden ...
Du hast in Maven ein paar Verzeichnisse, in die die Dateien deines Projekts gehören:

src/main/java für Java Dateien, die das eigentliche Projekt bilden
src/main/resources für Dateien, die als Ressourcen zu dem Projekt gehören.
src/test/java für die Unit Tests. Diese sind nicht Bestandteil der Auslieferung (Ebenso wie die Abhängigkeiten mit Scope: Test)
src/test/resources für Ressourcen, die bei den Tests verwendet werden und die auch nicht mit ausgeliefert werden soll.

Dein Test gehört somit nach src/test/java und nicht nach src/main/java.
 
So sieht nun handlypayedEvent aus. Wäre das in Ordnung?

Java:
public void handlePayedEvent(PayedEvent payedEvent) {
        payedEvent.getTeilnehmer().st=Status.ANGEMELDETBEZAHLT;
    }
 
Wäre das in Ordnung?
Ich denke nicht, siehe #16

Das Event ist einfach nur ein Ereignis. Es kann nicht wissen, wie der nächste Status des Teilnehmers sein muss und ob der sich überhaupt ändern soll. Das muss der Teilnehmer selbst wissen (oder eine vorhandene Domainlogik).

In deinem Fall z. B. setzt du den Status des Teilnehmers direkt auf ANGEMELDET_BEZAHLT, aber woher weißt du denn, dass der Teilnehmer bereits ANGEMELDET war?
 
Zuletzt bearbeitet:
Ich kann leider nicht mehr bearbeiten, darum

Persönlich würde ich es nicht so machen, wie im Schiff-Beispiel (die process() Methode im Event). Ich sehe ein Event als reine Datenklasse (Valueobject) an, ein PayedEvent sagt demnach nur aus, dass offenbar jemand bezahlt hat (nämlich der Teilnehmer). Mit diesen Informationen und aus der Reihenfolge der vorhandenen Events kann eine Klasse aus der Domänenlogik die Änderungen verarbeiten (sozusagen die Ereignisse abspielen). Das ist nur meine Meinung, aber die Anmerkung von @KonradN in #7 zielte wahrscheinlich in die selbe Richtung.
 
Na ja ich lasse es jetzt so. Ich denke, dass es für meine Zwecke ausreichend ist.
Es geht mir darum die Prüfung zu bestehen und nicht um die Erlangung von sinnvollem Wissen.

In einem weiteren Teil der Übungsaufgabe soll eine Möglichkeit entwickelt werden, die Events dauerhaft zu speichern.
Also eine Event-Persistenz soll implementiert werden.

Auf was wird da angespielt? Wie wird sowas typischerweise gespeichert?
 
Auf was wird da angespielt? Wie wird sowas typischerweise gespeichert?
Denk einfach mal an Dein Bankkonto (auch, wenn es vielleicht Albträume beschert, da musst Du durch). Dort werden Kontenbewegungen (= Events) aufgezeichnet. Startest Du mit einem leeren Konto und spulst die Events ab, kannst Du das Konto (inkl. Kontostand, versteht sich) für jeden beliebigen Zeitpunkt exakt wiederherstellen. Auf dem Kontoauszug siehst Du auch grob, wie das gespeichert wird.
 
Guten Tag,

nochmal eine Frage: ich speichere die Objekte (die Teilnehmer) in einem JPA Repository um dort dann Abfragen zu praktizieren.
Wenn dann ein Event ausgelöst wird, verbleibt das Objekt ja nun so wie es ist im Repository. Die Änderung des Events wird nicht berücksichtigt.

Das ist natürlich murks. Das Objekt löschen und neu hinzufügen, scheint mir auch nicht sinnvoll zu sein.
Was macht man da am besten?
 
Beim EventSourcing geht man von einem initialen Objekt aus (oder die Erzeugung des initialen Objektes ist bereits ein Event), auf dem man die Events der Reihe nach anwendet, bis man den aktuellen Status erreicht hat. Du musst also die Events speichern.
 

Zurück
Oben