web-dev-qa-db-fra.com

Utilisation d'un SGBDR comme stockage de source d'événements

Si j'utilisais un SGBDR (par exemple SQL Server) pour stocker des données de source d'événements, à quoi pourrait ressembler le schéma?

J'ai vu quelques variations évoquées dans un sens abstrait, mais rien de concret.

Par exemple, supposons que l'on possède une entité "Produit" et que les modifications apportées à ce produit puissent prendre la forme: prix, coût et description. Je ne sais pas si je:

  1. Avoir une table "ProductEvent", qui contient tous les champs d'un produit, où chaque modification signifie un nouvel enregistrement dans cette table, plus "qui, quoi, où, pourquoi, quand et comment" selon le cas. Lorsque le coût, le prix ou la description sont modifiés, une toute nouvelle ligne est ajoutée pour représenter le produit.
  2. Stockez le coût, le prix et la description du produit dans des tables distinctes jointes à la table produit avec une relation de clé étrangère. Lorsque des modifications sont apportées à ces propriétés, écrivez de nouvelles lignes avec WWWWWH selon le cas.
  3. Stocker WWWWWH, plus un objet sérialisé représentant l'événement, dans une table "ProductEvent", ce qui signifie que l'événement lui-même doit être chargé, désérialisé et relu dans mon code d'application afin de reconstruire l'état de l'application pour un produit donné .

Je m'inquiète particulièrement de l'option 2 ci-dessus. Poussé à l'extrême, la table de produits serait presque une table par propriété, où pour charger l'état d'application pour un produit donné, il faudrait charger tous les événements pour ce produit à partir de chaque table d'événements de produit. Cette explosion de table me sent mal.

Je suis sûr que "cela dépend", et bien qu'il n'y ait pas de "bonne réponse", j'essaie d'avoir une idée de ce qui est acceptable et de ce qui est totalement inacceptable. Je suis également conscient que NoSQL peut aider ici, où les événements pourraient être stockés par rapport à une racine agrégée, ce qui signifie qu'une seule demande à la base de données pour obtenir les événements à partir desquels reconstruire l'objet, mais nous n'utilisons pas de base de données NoSQL à la moment donc je me sens autour pour des alternatives.

104
Neil Barnwell

Le magasin d'événements ne doit pas avoir besoin de connaître les champs ou propriétés spécifiques des événements. Sinon, chaque modification de votre modèle entraînerait la migration de votre base de données (tout comme dans une bonne persistance basée sur l'état). Par conséquent, je ne recommanderais pas du tout les options 1 et 2.

Voici le schéma utilisé dans Ncqrs . Comme vous pouvez le voir, le tableau "Événements" stocke les données associées sous forme de CLOB (c'est-à-dire JSON ou XML). Cela correspond à votre option 3 (seulement qu'il n'y a pas de table "ProductEvents" car vous n'avez besoin que d'une seule table "Events" générique. Dans Ncqrs, le mappage vers vos racines d'agrégation se fait via la table "EventSources", où chaque EventSource correspond à une réelle Racine agrégée.)

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

Le mécanisme de persistance SQL de implémentation Event Store de Jonathan Oliver consiste essentiellement en une table appelée "Commits" avec un champ BLOB "Payload". C'est à peu près la même chose que dans Ncqrs, seulement qu'il sérialise les propriétés de l'événement au format binaire (ce qui, par exemple, ajoute la prise en charge du chiffrement).

Greg Young recommande une approche similaire, comme largement documenté sur le site Web de Greg .

Le schéma de sa table prototypique "Événements" se lit comme suit:

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

Le projet GitHub CQRS.NET a quelques exemples concrets de la façon dont vous pourriez faire EventStores dans quelques technologies différentes. Au moment de l'écriture, il y a une implémentation en SQL utilisant Linq2SQL et un schéma SQL pour aller avec, il y en a une pour MongoDB , une pour - DocumentDB (CosmosDB si vous êtes dans Azure) et un utilisant EventStore (comme mentionné ci-dessus). Il y a plus dans Azure comme le stockage de table et le stockage Blob qui est très similaire au stockage de fichiers plats.

Je suppose que le point principal ici est qu'ils sont tous conformes au même principe/contrat. Ils stockent tous des informations dans un seul endroit/conteneur/table, ils utilisent des métadonnées pour identifier un événement à partir d'un autre et stockent `` simplement '' tout l'événement tel qu'il était - dans certains cas, sérialisé, dans les technologies de support, tel qu'il était. Donc, selon que vous choisissez une base de données de documents, une base de données relationnelle ou même un fichier plat, il existe plusieurs façons d'atteindre la même intention d'un magasin d'événements (c'est utile si vous changez d'avis à tout moment et trouvez que vous devez migrer ou prendre en charge plusieurs technologies de stockage).

En tant que développeur du projet, je peux partager quelques idées sur certains des choix que nous avons faits.

Tout d'abord, nous avons trouvé (même avec des UUID/GUID uniques au lieu d'entiers) pour de nombreuses raisons que les ID séquentiels se produisent pour des raisons stratégiques, donc le simple fait d'avoir un ID n'était pas assez unique pour une clé, nous avons donc fusionné notre colonne principale de clé d'ID avec les données/type d'objet pour créer ce qui devrait être une clé vraiment unique (au sens de votre application). Je sais que certaines personnes disent que vous n'avez pas besoin de le stocker, mais cela dépendra si vous êtes greenfield ou si vous devez coexister avec les systèmes existants.

Nous sommes restés avec un seul conteneur/table/collection pour des raisons de maintenabilité, mais nous avons joué avec une table distincte par entité/objet. Nous avons trouvé dans la pratique que cela signifiait que l'application avait besoin d'autorisations "CRÉER" (ce qui n'est généralement pas une bonne idée ... généralement, il y a toujours des exceptions/exclusions) ou chaque fois qu'une nouvelle entité/un nouvel objet est apparu ou a été déployé, nouveau des conteneurs/tables/collections de stockage devaient être créés. Nous avons constaté que cela était douloureusement lent pour le développement local et problématique pour les déploiements de production. Peut-être pas, mais c'était notre expérience du monde réel.

Une autre chose à retenir est que le fait de demander à l'action X de se produire peut entraîner de nombreux événements différents, connaissant ainsi tous les événements générés par une commande/un événement/ce qui est utile. Ils peuvent également se trouver sur différents types d'objets, par exemple pousser "acheter" dans un panier peut déclencher des événements de compte et d'entreposage. Une application consommatrice peut vouloir savoir tout cela, nous avons donc ajouté un CorrelationId. Cela signifiait qu'un consommateur pouvait demander tous les événements soulevés à la suite de sa demande. Vous le verrez dans le schéma .

Plus précisément avec SQL, nous avons constaté que les performances devenaient vraiment un goulot d'étranglement si les index et les partitions n'étaient pas correctement utilisés. N'oubliez pas que les événements devront être diffusés dans l'ordre inverse si vous utilisez des instantanés. Nous avons essayé quelques index différents et avons constaté qu'en pratique, certains index supplémentaires étaient nécessaires pour déboguer des applications réelles en production. Encore une fois, vous verrez que dans le schéma .

D'autres métadonnées en cours de production ont été utiles lors des enquêtes basées sur la production, les horodatages nous ont donné un aperçu de l'ordre dans lequel les événements étaient persistants vs déclenchés. Cela nous a donné de l'aide sur un système particulièrement axé sur les événements qui a déclenché de grandes quantités d'événements, nous donnant des informations sur la performance de choses comme les réseaux et la distribution des systèmes à travers le réseau.

7
cdmdotnet

Eh bien, vous voudrez peut-être jeter un œil à Datomic.

Datomic est une base de données flexible, faits basés sur le temps, prenant en charge les requêtes et les jointures, avec une évolutivité élastique et ACID transactions.

J'ai écrit une réponse détaillée ici

Vous pouvez regarder une conférence de Stuart Halloway expliquant la conception de Datomic ici

Étant donné que Datomic stocke les faits dans le temps, vous pouvez l'utiliser pour des cas d'utilisation de sourcing d'événements, et bien plus encore.

3
kisai

Un indice possible est la conception suivie de "Dimension à changement lent" (type = 2) devrait vous aider à couvrir:

  • ordre des événements qui se produisent (via la clé de substitution)
  • durabilité de chaque état (valable du - valable au)

La fonction de repli gauche devrait également être mise en œuvre, mais vous devez penser à la complexité future des requêtes.

1
Viktor Nakonechnyy

Je pense que la solution (1 & 2) peut devenir un problème très rapidement à mesure que votre modèle de domaine évolue. De nouveaux champs sont créés, certains changent de sens et certains peuvent ne plus être utilisés. Finalement, votre table aura des dizaines de champs nullables, et le chargement des événements sera un gâchis.

N'oubliez pas non plus que le magasin d'événements ne doit être utilisé que pour les écritures, vous l'interrogez uniquement pour charger les événements, pas les propriétés de l'agrégat. Ce sont des choses distinctes (c'est l'essence du CQRS).

Solution 3 ce que les gens font habituellement, il y a plusieurs façons de le faire.

Par exemple, EventFlow CQRS lorsqu'il est utilisé avec SQL Server crée une table avec ce schéma:

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
)

où:

  • GlobalSequenceNumber : identification globale simple, peut être utilisée pour classer ou identifier les événements manquants lorsque vous créez votre projection (modèle de lecture).
  • BatchId : une identification du groupe d'événements inséré atomiquement (TBH, je ne sais pas pourquoi cela serait utile)
  • AggregateId : Identification de l'agrégat
  • Données : événement sérialisé
  • Métadonnées : autres informations utiles de l'événement (par exemple, type d'événement utilisé pour la désérialisation, horodatage, identifiant de la commande, etc.)
  • AggregateSequenceNumber : numéro de séquence dans le même agrégat (utile si vous ne pouvez pas avoir d'écrits en panne, vous devez donc utiliser ce champ pour une concurrence optimiste)

Cependant, si vous créez à partir de zéro, je recommanderais de suivre le principe YAGNI et de créer avec le minimum de champs requis pour votre cas d'utilisation.

0
Fabio Marreco