it-swarm.com.de

Verwendung eines RDBMS als Event-Sourcing-Speicher

Wie könnte das Schema aussehen, wenn ich ein RDBMS (z. B. SQL Server) zum Speichern von Ereignisbeschaffungsdaten verwende?

Ich habe ein paar Variationen gesehen, über die abstrakt gesprochen wurde, aber nichts Konkretes.

Nehmen wir zum Beispiel an, man hat eine "Produkt" -Entität und Änderungen an diesem Produkt könnten in Form von: Preis, Kosten und Beschreibung erfolgen. Ich bin verwirrt, ob ich:

  1. Haben Sie eine "ProductEvent" -Tabelle, die alle Felder für ein Produkt enthält, wobei jede Änderung einen neuen Datensatz in dieser Tabelle bedeutet, sowie "wer, was, wo, warum, wann und wie". Wenn Kosten, Preis oder Beschreibung geändert werden, wird eine neue Zeile hinzugefügt, um das Produkt darzustellen.
  2. Speichern Sie Produktkosten, -preis und -beschreibung in separaten Tabellen, die mit der Produkttabelle mit einer Fremdschlüsselbeziehung verbunden sind. Wenn Änderungen an diesen Eigenschaften vorgenommen werden, schreiben Sie neue Zeilen mit WWWWWH.
  3. Speichern Sie WWWWWH sowie ein serialisiertes Objekt, das das Ereignis darstellt, in einer "ProductEvent" -Tabelle. Dies bedeutet, dass das Ereignis selbst in meinen Anwendungscode geladen, de-serialisiert und erneut abgespielt werden muss, um den Anwendungsstatus für ein bestimmtes Produkt neu zu erstellen .

Insbesondere mache ich mir Sorgen über Option 2 oben. Im Extremfall ist die Produkttabelle fast eine Tabelle pro Eigenschaft. Wenn der Anwendungsstatus für ein bestimmtes Produkt geladen werden soll, müssen alle Ereignisse für dieses Produkt aus jeder Produktereignistabelle geladen werden. Diese Tischexplosion riecht für mich falsch.

Ich bin sicher, "es kommt darauf an", und obwohl es keine einzige "richtige Antwort" gibt, versuche ich, ein Gefühl dafür zu bekommen, was akzeptabel und was absolut nicht akzeptabel ist. Mir ist auch bewusst, dass NoSQL hier helfen kann, wo Ereignisse gegen einen aggregierten Stamm gespeichert werden können, was bedeutet, dass nur eine einzige Anforderung an die Datenbank gesendet wird, um die Ereignisse zum erneuten Erstellen des Objekts abzurufen. Wir verwenden jedoch keine NoSQL-Datenbank im Moment also bin ich auf der Suche nach Alternativen.

104
Neil Barnwell

Der Ereignisspeicher muss nicht über die spezifischen Felder oder Eigenschaften von Ereignissen informiert sein. Andernfalls würde jede Änderung Ihres Modells dazu führen, dass Ihre Datenbank migriert werden muss (genau wie bei einer guten, altmodischen zustandsbasierten Persistenz). Daher würde ich Option 1 und 2 überhaupt nicht empfehlen.

Unten ist das Schema, wie es in Ncqrs verwendet wird. Wie Sie sehen können, speichert die Tabelle "Ereignisse" die zugehörigen Daten als CLOB (d. H. JSON oder XML). Dies entspricht Ihrer Option 3 (nur, dass es keine Tabelle "ProductEvents" gibt, da Sie nur eine generische Tabelle "Events" benötigen. In Ncqrs erfolgt die Zuordnung zu Ihren Aggregate Roots über die Tabelle "EventSources", wobei jede EventSource einem tatsächlichen Wert entspricht Gesamtwurzel.)

Table Events:
    Id [uniqueidentifier] NOT NULL,
    TimeStamp [datetime] NOT NULL,

    Name [varchar](max) NOT NULL,
    Version [varchar](max) NOT NULL,

    EventSourceId [uniqueidentifier] NOT NULL,
    Sequence [bigint], 

    Data [nvarchar](max) NOT NULL

Table EventSources:
    Id [uniqueidentifier] NOT NULL, 
    Type [nvarchar](255) NOT NULL, 
    Version [int] NOT NULL

Der SQL-Persistenzmechanismus von Jonathan Olivers Event Store-Implementierung besteht im Wesentlichen aus einer Tabelle namens "Commits" mit einem BLOB-Feld "Payload". Dies ist so ziemlich dasselbe wie in Ncqrs, nur dass die Eigenschaften des Ereignisses im Binärformat serialisiert werden (wodurch beispielsweise Verschlüsselungsunterstützung hinzugefügt wird).

Greg Young empfiehlt einen ähnlichen Ansatz, wie ausführlich auf Gregs Website dokumentiert .

Das Schema seiner prototypischen "Events" -Tabelle lautet:

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]
99
Dennis Traub

Das GitHub-Projekt CQRS.NET enthält einige konkrete Beispiele, wie Sie EventStores mit einigen verschiedenen Technologien erstellen können. Zum Zeitpunkt des Schreibens gibt es eine Implementierung in SQL mit Linq2SQL und ein SQL-Schema dazu gibt es eine für MongoDB , eine für - DocumentDB (CosmosDB, wenn Sie in Azure arbeiten) und eine mit EventStore (wie oben erwähnt). In Azure gibt es noch mehr wie den Tabellenspeicher und den Blob-Speicher, die dem Flat-File-Speicher sehr ähnlich sind.

Ich denke, der wichtigste Punkt hier ist, dass sie alle dem gleichen Auftraggeber/Vertrag entsprechen. Sie alle speichern Informationen an einem einzigen Ort/in einem Container/in einer Tabelle, sie verwenden Metadaten, um ein Ereignis von einem anderen zu unterscheiden und speichern das gesamte Ereignis so, wie es war - in einigen Fällen serialisiert, in unterstützenden Technologien, wie es war. Je nachdem, ob Sie eine Dokumentendatenbank, eine relationale Datenbank oder sogar eine Einfachdatei auswählen, gibt es verschiedene Möglichkeiten, um die gleiche Absicht eines Ereignisspeichers zu erreichen. Dies ist hilfreich, wenn Sie Ihre Meinung zu einem beliebigen Zeitpunkt ändern und feststellen, dass Sie migrieren oder Support benötigen mehr als eine Speichertechnologie).

Als Entwickler des Projekts kann ich einige Einblicke in einige der von uns getroffenen Entscheidungen geben.

Erstens haben wir festgestellt, dass (auch bei eindeutigen UUIDs/GUIDs anstelle von Ganzzahlen) aus vielen Gründen aus strategischen Gründen sequentielle IDs auftreten, sodass nur eine ID für einen Schlüssel nicht eindeutig genug war. Deshalb haben wir unsere Haupt-ID-Schlüsselspalte mit den Daten/Objekttyp, um einen wirklich (im Sinne Ihrer Anwendung) eindeutigen Schlüssel zu erstellen. Ich weiß, dass einige Leute sagen, dass Sie es nicht speichern müssen, aber das hängt davon ab, ob Sie auf der grünen Wiese sind oder mit vorhandenen Systemen koexistieren müssen.

Wir haben uns aus Gründen der Wartbarkeit an einen einzelnen Container/eine einzelne Tabelle/Sammlung gehalten, aber wir haben mit einer separaten Tabelle pro Entität/Objekt herumgespielt. Wir haben in der Praxis festgestellt, dass entweder die Anwendung die Berechtigung "CREATE" benötigt (was im Allgemeinen keine gute Idee ist ... im Allgemeinen gibt es immer Ausnahmen/Ausschlüsse) oder jedes Mal, wenn eine neue Entität/ein neues Objekt entsteht oder bereitgestellt wird, neu Lagerbehälter/Tische/Sammlungen mussten hergestellt werden. Wir fanden, dass dies für die lokale Entwicklung schmerzlich langsam und für Produktionsbereitstellungen problematisch war. Sie können nicht, aber das war unsere reale Erfahrung.

Sie sollten sich auch daran erinnern, dass das Auffordern von Aktion X zum Eintreten vieler verschiedener Ereignisse führen kann, sodass Sie alle Ereignisse kennen, die von einem Befehl/Ereignis/je nachdem, was nützlich ist, generiert wurden. Sie können sich auch über verschiedene Objekttypen erstrecken, z. Das Drücken von "Kaufen" in einem Einkaufswagen kann Konto- und Lagerereignisse auslösen. Eine konsumierende Anwendung möchte dies alles wissen, daher haben wir eine CorrelationId hinzugefügt. Dies bedeutete, dass ein Verbraucher nach allen Ereignissen fragen konnte, die aufgrund seiner Anfrage ausgelöst wurden. Das sehen Sie im Schema .

Insbesondere bei SQL stellte sich heraus, dass die Leistung zu einem echten Engpass wurde, wenn Indizes und Partitionen nicht ausreichend genutzt wurden. Denken Sie daran, dass Ereignisse in umgekehrter Reihenfolge gestreamt werden müssen, wenn Sie Snapshots verwenden. Wir haben einige verschiedene Indizes ausprobiert und festgestellt, dass in der Praxis einige zusätzliche Indizes zum Debuggen von produktionsinternen realen Anwendungen erforderlich sind. Das sehen Sie wieder im Schema .

Andere produktionsinterne Metadaten waren bei produktionsbasierten Untersuchungen hilfreich. Zeitstempel gaben uns einen Einblick in die Reihenfolge, in der Ereignisse fortbestehen oder ausgelöst werden. Dies hat uns bei einem besonders ereignisgesteuerten System geholfen, das eine Vielzahl von Ereignissen auslöste und uns Informationen über die Leistung von Dingen wie Netzwerken und die Systemverteilung über das Netzwerk lieferte.

7
cdmdotnet

Nun, vielleicht möchten Sie einen Blick auf Datomic werfen.

Datomic ist eine Datenbank mit flexiblen zeitbasierten Fakten, die Abfragen und Verknüpfungen mit elastischer Skalierbarkeit und ACID unterstützt Transaktionen.

Ich habe eine ausführliche Antwort geschrieben hier

Sie können sich einen Vortrag von Stuart Halloway ansehen, in dem das Design von Datomic erklärt wird hier

Da Datomic Fakten rechtzeitig speichert, können Sie sie für Anwendungsfälle der Ereignisbeschaffung und vieles mehr verwenden.

3
kisai

Möglicher Hinweis ist, dass das Design gefolgt von "Langsam ändernde Bemaßung" (Typ = 2) Ihnen dabei helfen sollte, Folgendes abzudecken:

  • reihenfolge der Ereignisse (über Ersatzschlüssel)
  • haltbarkeit jedes Staates (gültig von - gültig bis)

Die Implementierung der Funktion für die linke Falte sollte ebenfalls in Ordnung sein, Sie müssen jedoch über die zukünftige Komplexität der Abfrage nachdenken.

1

Ich denke, Lösung (1 & 2) kann sehr schnell zu einem Problem werden, wenn sich Ihr Domain-Modell weiterentwickelt. Es werden neue Felder erstellt, einige ändern ihre Bedeutung, andere können nicht mehr verwendet werden. Schließlich wird Ihre Tabelle Dutzende von nullwertfähigen Feldern haben, und das Laden der Ereignisse wird chaotisch sein.

Denken Sie auch daran, dass der Ereignisspeicher nur für Schreibvorgänge verwendet werden sollte. Sie fragen ihn nur ab, um die Ereignisse zu laden, nicht die Eigenschaften des Aggregats. Sie sind getrennte Dinge (das ist die Essenz von CQRS).

Lösung 3 Was Menschen normalerweise tun, gibt es viele Möglichkeiten, dies zu erreichen.

Beispiel: EventFlow CQRS erstellt bei Verwendung mit SQL Server eine Tabelle mit dem folgenden Schema:

CREATE TABLE [dbo].[EventFlow](
    [GlobalSequenceNumber] [bigint] IDENTITY(1,1) NOT NULL,
    [BatchId] [uniqueidentifier] NOT NULL,
    [AggregateId] [nvarchar](255) NOT NULL,
    [AggregateName] [nvarchar](255) NOT NULL,
    [Data] [nvarchar](max) NOT NULL,
    [Metadata] [nvarchar](max) NOT NULL,
    [AggregateSequenceNumber] [int] NOT NULL,
 CONSTRAINT [PK_EventFlow] PRIMARY KEY CLUSTERED 
(
    [GlobalSequenceNumber] ASC
)

wo:

  • GlobalSequenceNumber : Einfache globale Identifikation, kann zum Ordnen oder Identifizieren der fehlenden Ereignisse beim Erstellen Ihrer Projektion (Readmodel) verwendet werden.
  • BatchId : Eine Identifizierung der Gruppe von Ereignissen, die atomar eingefügt wurden (TBH, keine Ahnung, warum dies nützlich wäre)
  • AggregateId : Identifizierung des Aggregats
  • Daten : Serialisiertes Ereignis
  • Metadaten : Andere nützliche Informationen aus dem Ereignis (z. B. Ereignistyp für Deserialisierung, Zeitstempel, Absender-ID aus Befehl usw.)
  • AggregateSequenceNumber : Sequenznummer innerhalb desselben Aggregats (Dies ist nützlich, wenn keine Schreibvorgänge in falscher Reihenfolge ausgeführt werden können. Verwenden Sie dieses Feld daher für eine optimistische Parallelität.)

Wenn Sie jedoch von Grund auf neu erstellen, würde ich empfehlen, dem YAGNI-Prinzip zu folgen und mit den minimal erforderlichen Feldern für Ihren Anwendungsfall zu erstellen.

0
Fabio Marreco