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


119

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

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

Par exemple, disons que l'on a une entité «Produit» et que les modifications apportées à ce produit peuvent prendre la forme de: Prix, Coût et Description. Je ne sais pas si je:

  1. Avoir une table "ProductEvent", qui contient tous les champs pour un produit, où chaque changement signifie un nouvel enregistrement dans cette table, plus "qui, quoi, où, pourquoi, quand et comment" (WWWWWH) le cas échéant. 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 séparées jointes à la table Product avec une relation de clé étrangère. Lorsque des modifications de ces propriétés se produisent, écrivez de nouvelles lignes avec WWWWWH, le cas échéant.
  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 recréer l'état de l'application pour un produit donné .

En particulier, je m'inquiète de l'option 2 ci-dessus. Poussée à l'extrême, la table de produits serait presque une table par propriété, où charger l'état d'application pour un produit donné nécessiterait de charger tous les événements pour ce produit à partir de chaque table d'événements de produit. Cette explosion de table me fait mal.

Je suis sûr que «cela dépend», et bien qu'il n'y ait pas de «bonne réponse» unique, j'essaie d'avoir une idée de ce qui est acceptable et de ce qui ne l'est pas du tout. Je suis également conscient que NoSQL peut aider ici, où les événements peuvent être stockés sur une racine agrégée, ce qui signifie qu'une seule demande à la base de données pour obtenir les événements à partir de laquelle reconstruire l'objet, mais nous n'utilisons pas de base de données NoSQL à la moment donc je cherche des alternatives.


2
Dans sa forme la plus simple: [Event] {AggregateId, AggregateVersion, EventPayload}. Pas besoin du type d'agrégat, mais vous POUVEZ éventuellement le stocker. Pas besoin de type d'événement, mais vous POUVEZ éventuellement le stocker. C'est une longue liste de choses qui se sont produites, tout le reste n'est que de l'optimisation.
Yves Reynhout

7
Évitez les n ° 1 et n ° 2. Sérialisez tout en un objet blob et stockez-le de cette façon.
Jonathan Oliver

Réponses:


109

Le magasin d'événements ne devrait pas avoir besoin de connaître les champs ou les propriétés spécifiques des événements. Sinon, toute modification de votre modèle entraînerait la migration de votre base de données (tout comme dans le cas d'une bonne persistance basée sur l'état à l'ancienne). 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, la table "Evénements" stocke les données associées sous forme de CLOB (ie 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 table "Events" générique. Dans Ncqrs, le mappage vers vos racines agrégées se fait via la table "EventSources", où chaque EventSource correspond à un réel 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 l'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, mais 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ée 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]

9
Bonne réponse! L'un des principaux arguments que je continue de lire pour utiliser EventSourcing est la possibilité d'interroger l'historique. Comment vais-je créer un outil de création de rapports efficace pour interroger lorsque toutes les données intéressantes sont sérialisées au format XML ou JSON? Y a-t-il des articles intéressants à la recherche d'une solution basée sur une table?
Marijn Huizendveld

11
@MarijnHuizendveld, vous ne voulez probablement pas interroger le magasin d'événements lui-même. La solution la plus courante consiste à connecter deux gestionnaires d'événements qui projettent les événements dans une base de données de rapports ou de BI. La relecture de l'historique des événements contre ces gestionnaires.
Dennis Traub

1
@Denis Traub merci pour votre réponse. Pourquoi ne pas interroger le magasin d'événements lui-même? J'ai peur que cela devienne assez compliqué / intense si nous devons rejouer l'historique complet à chaque fois que nous proposons un nouveau cas BI?
Marijn Huizendveld

1
Je pensais qu'à un moment donné, vous étiez censé avoir également des tables en plus du magasin d'événements, pour stocker les données du modèle dans son dernier état? Et que vous divisez le modèle en un modèle de lecture et un modèle d'écriture. Le modèle d'écriture va à l'encontre du magasin d'événements et les martiaux du magasin d'événements mettent à jour le modèle de lecture. Le modèle de lecture contient les tables qui représentent les entités de votre système. Vous pouvez donc utiliser le modèle de lecture pour créer des rapports et afficher. J'ai dû mal comprendre quelque chose.
theBoringCoder

10
@theBoringCoder On dirait que vous avez confondu Event Sourcing et CQRS ou du moins écrasé dans votre tête. On les trouve fréquemment ensemble mais ce n'est pas la même chose. CQRS vous permet de séparer vos modèles de lecture et d'écriture tandis que Event Sourcing vous permet d'utiliser un flux d'événements comme source unique de vérité dans votre application.
Bryan Anderson

7

Le projet GitHub CQRS.NET a quelques exemples concrets de la façon dont vous pourriez faire des EventStores dans quelques technologies différentes. Au moment de la rédaction de cet article, il existe une implémentation en SQL utilisant Linq2SQL et un schéma SQL qui va avec, il y en a un pour MongoDB , un 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 principal / contrat. Ils stockent tous les informations dans un seul endroit / conteneur / table, ils utilisent des métadonnées pour identifier un événement à partir d'un autre et `` juste '' stocker l'événement entier tel qu'il était - dans certains cas sérialisé, dans des technologies de support, pour ainsi dire. 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 que vous avez besoin de migrer ou de 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.

Premièrement, nous avons trouvé (même avec des UUID / GUID uniques au lieu d'entiers) pour de nombreuses raisons, les ID séquentiels se produisent pour des raisons stratégiques, ainsi le simple fait d'avoir un ID n'était pas assez unique pour une clé, nous avons donc fusionné notre colonne de clé ID principale 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 du fait que vous soyez un nouveau site ou que vous deviez coexister avec des systèmes existants.

Nous nous sommes contentés d'un seul conteneur / table / collection pour des raisons de maintenabilité, mais nous avons joué avec une table séparée par entité / objet. Nous avons trouvé dans la pratique que cela signifiait que l'application avait besoin d'autorisations "CREATE" (ce qui n'est généralement pas une bonne idée ... en général, il y a toujours des exceptions / exclusions) ou à chaque fois qu'une nouvelle entité / objet est apparue ou a été déployée, une nouvelle des conteneurs / tables / collections de stockage doivent être réalisés. Nous avons constaté que cela était extrêmement lent pour le développement local et problématique pour les déploiements de production. Vous ne pouvez pas, mais c'était notre expérience du monde réel.

Une autre chose à retenir est que 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 concerner 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 verrez cela 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 doivent ê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 les applications du monde réel en production. Encore une fois, vous verrez cela dans le schéma .

D'autres métadonnées en 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 ont été persistés par rapport à leur déclenchement. Cela nous a donné une certaine assistance sur un système particulièrement axé sur les événements qui a soulevé de grandes quantités d'événements, nous donnant des informations sur les performances de choses comme les réseaux et la distribution des systèmes sur le réseau.


C'est génial merci. En fait, depuis longtemps depuis que j'ai écrit cette question, j'en ai construit moi-même quelques-uns dans le cadre de ma bibliothèque Inforigami.Regalo sur github. Implémentations RavenDB, SQL Server et EventStore. Je me suis demandé d'en faire un basé sur un fichier, pour rire. :)
Neil Barnwell

1
À votre santé. J'ai ajouté la réponse principalement pour ceux qui l'ont rencontrée plus récemment et qui partagent certaines des leçons apprises, plutôt que simplement le résultat.
cdmdotnet

3

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

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

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 source d'événements, et bien plus encore.


2

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 Nullable, et le chargement des événements sera désordonné.

Souvenez-vous également que le magasin d'événements ne doit être utilisé que pour les écritures, vous ne l'interrogez que pour charger les événements, pas les propriétés de l'agrégat. Ce sont des choses séparées (c'est l'essence même du CQRS).

Solution 3 ce que les gens font habituellement, il existe de nombreuses façons d'y parvenir.

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 ordonner ou identifier les événements manquants lorsque vous créez votre projection (readmodel).
  • BatchId : Une identification du groupe d'événements qui ont été insérés de manière atomique (TBH, je n'ai aucune idée de pourquoi cela serait utile)
  • AggregateId : Identification de l'agrégat
  • Données : événement sérialisé
  • Métadonnées : autres informations utiles sur l'événement (par exemple, le type d'événement utilisé pour la désérialisation, l'horodatage, l'ID de l'expéditeur de la commande, etc.)
  • AggregateSequenceNumber : Numéro de séquence dans le même agrégat (cela est utile si vous ne pouvez pas avoir d'écritures dans le désordre, vous utilisez donc ce champ pour une concurrence optimiste)

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


Je dirais que BatchId pourrait potentiellement être lié à CorrelationId et CausationId. Utilisé pour déterminer ce qui a causé les événements et les enchaîner si nécessaire.
Daniel Park le

Il pourrait être. Quoi qu'il en soit, il serait logique de fournir un moyen de le personnaliser (par exemple en définissant comme identifiant de la demande), mais le framework ne le fait pas.
Fabio Marreco

1

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

  • ordre des événements survenant (via une clé de substitution)
  • durabilité de chaque état (valide de - valide à)

La fonction de pliage à gauche devrait également être correcte à implémenter, mais vous devez penser à la complexité future des requêtes.


1

Je pense que ce serait une réponse tardive, mais je tiens à souligner que l'utilisation du SGBDR comme stockage de source d'événements est tout à fait possible si votre exigence de débit n'est pas élevée. Je voudrais simplement vous montrer des exemples d'un grand livre de recherche d'événements que je construis pour illustrer.

https://github.com/andrewkkchan/client-ledger-service Ce qui précède est un service Web de registre de recherche d'événements. https://github.com/andrewkkchan/client-ledger-core-db Et ce qui précède, j'utilise le SGBDR pour calculer les états afin que vous puissiez profiter de tous les avantages d'un SGBDR comme le support des transactions. https://github.com/andrewkkchan/client-ledger-core-memory Et j'ai un autre consommateur à traiter en mémoire pour gérer les rafales.

On dirait que le magasin d'événements ci-dessus vit toujours à Kafka - car le SGBDR est lent à insérer, en particulier lorsque l'insertion est toujours en cours.

J'espère que le code vous aidera à vous donner une illustration en dehors des très bonnes réponses théoriques déjà fournies pour cette question.


Merci. J'ai depuis longtemps construit une implémentation basée sur SQL. Je ne sais pas pourquoi un SGBDR est lent pour les insertions, sauf si vous avez fait un choix inefficace pour une clé en cluster quelque part. Ajouter uniquement devrait être bien.
Neil Barnwell
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.