Réhydrater les agrégats à partir d'une projection de «clichés» plutôt que du magasin d'événements

14

Je flirte donc avec Event Sourcing et CQRS depuis un moment maintenant, même si je n'ai jamais eu l'occasion d'appliquer les patterns sur un vrai projet.

Je comprends les avantages de séparer vos préoccupations en lecture et en écriture, et j'apprécie la façon dont Event Sourcing facilite la projection de changements d'état dans les bases de données "Read Model" qui sont différentes de votre magasin d'événements.

Ce que je ne sais pas très bien, c'est pourquoi vous réhydrateriez jamais vos agrégats à partir du magasin d'événements lui-même.

Si la projection de modifications dans des bases de données "en lecture" est si facile, pourquoi ne pas toujours projeter des modifications dans une base de données "en écriture" dont le schéma correspond parfaitement à votre modèle de domaine? Il s'agirait effectivement d'une base de données d'instantanés.

J'imagine que cela doit être assez courant dans les applications ES + CQRS à l'état sauvage.

Si tel est le cas, le magasin d'événements n'est-il utile que lors de la reconstruction de votre base de données "d'écriture" à la suite de modifications de schéma? Ou est-ce que je manque quelque chose de plus grand?

MetaFight
la source
Il n'y a rien de mal à écrire de manière asynchrone dans un magasin d'état de modèle d'écriture et à l'utiliser exclusivement pour charger des entités. Les mêmes problèmes de cohérence sont présents, que vous le fassiez ou non. La clé pour résoudre ces problèmes de cohérence consiste à modéliser vos entités différemment. Il n'y a rien de magique dans Event Sourcing qui résout ces problèmes de cohérence. La magie réside dans la modélisation et ne pas se soucier. Il existe des applications spécifiques qui nécessitent une cohérence à ce niveau et qui ont des entités très litigieuses quelle que soit la façon dont vous les modélisez et qui nécessiteront une attention particulière.
Andrew Larsson
Tant que vous pouvez garantir la livraison des événements. Pour ce faire, votre application doit simplement publier un événement de manière synchrone sur un bus d'événements durable. Après la publication, le travail de l'application est terminé. Le bus le remettra ensuite à divers gestionnaires d'événements: un pour mettre à jour le magasin d'événements, un pour mettre à jour le magasin d'état et tous les autres nécessaires pour mettre à jour les magasins de lecture. La raison pour laquelle vous utilisez Event Sourcing est que vous ne vous souciez plus de la cohérence immédiate. Embrasse le.
Andrew Larsson
Il n'y a aucune raison de charger constamment vos entités depuis le magasin d'événements. Ce n'est pas son but. Son but est de fournir un registre brut et permanent de tout ce qui s'est produit dans le système. Les magasins d'état d'entité et les modèles de lecture dénormalisés sont destinés au chargement et à la lecture.
Andrew Larsson

Réponses:

14

Ce que je ne sais pas très bien, c'est pourquoi vous réhydrateriez jamais vos agrégats à partir du magasin d'événements lui-même.

Parce que les "événements" sont le livre des records.

Si la projection de modifications dans des bases de données "en lecture" est si facile, pourquoi ne pas toujours projeter des modifications dans une base de données "en écriture" dont le schéma correspond parfaitement à votre modèle de domaine? Il s'agirait effectivement d'une base de données d'instantanés.

Oui; il est parfois utile d'optimiser les performances pour utiliser une copie mise en cache de l'état agrégé, plutôt que de régénérer cet état à partir de zéro à chaque fois. N'oubliez pas: la première règle d'optimisation des performances est "Ne pas". Cela ajoute une complexité supplémentaire à la solution, et vous préféreriez éviter cela jusqu'à ce que vous ayez une motivation commerciale convaincante.

Si tel est le cas, le magasin d'événements n'est-il utile que lors de la reconstruction de votre base de données "d'écriture" à la suite de modifications de schéma? Ou est-ce que je manque quelque chose de plus grand?

Il vous manque quelque chose de plus gros.

Le premier point est que si vous envisagez une solution issue d'un événement, c'est parce que vous vous attendez à ce qu'il y ait de la valeur à conserver l'historique de ce qui s'est passé, c'est-à-dire que vous souhaitez apporter des modifications non destructives .

C'est pourquoi nous écrivons à la boutique d'événements.

En particulier, cela signifie que chaque modification doit être écrite dans le magasin d'événements.

Les écrivains concurrents pourraient potentiellement soit détruire les écritures de l'autre, soit conduire le système à un état involontaire, s'ils ne sont pas au courant des modifications de l'autre. Ainsi, l'approche habituelle lorsque vous avez besoin de cohérence est d'adresser vos écritures à une position spécifique dans le journal (analogue à un PUT conditionnel dans une API HTTP). Une écriture qui a échoué indique à l'écrivain que sa compréhension actuelle du journal n'est pas synchronisée et qu'il devrait récupérer.

Revenir à une bonne position connue puis rejouer tous les événements supplémentaires, car ce point est une stratégie de récupération commune. Cette bonne position connue peut être une copie de ce qui se trouve dans le cache local ou une représentation dans votre magasin d'instantanés.

Sur la bonne voie, vous pouvez conserver un instantané de l'agrégat en mémoire; il vous suffit de vous adresser à un magasin externe lorsqu'il n'y a pas de copie locale disponible.

De plus, vous n'avez pas besoin d'être complètement rattrapé si vous avez accès au livre des records.

Ainsi, l'approche habituelle ( si vous utilisez un référentiel d'instantanés) consiste à le maintenir de manière asynchrone . De cette façon, si vous avez besoin de récupérer, vous pouvez le faire sans recharger et rejouer tout l'historique de l'agrégat.

Il existe de nombreux cas où cette complexité n'est pas intéressante, car les agrégats à grain fin avec des durées de vie limitées ne collectent généralement pas suffisamment d'événements pour que les avantages dépassent les coûts de maintenance d'un cache d'instantanés.

Mais quand c'est le bon outil pour le problème, charger une représentation périmée de l'agrégat dans le modèle d'écriture, puis le mettre à jour avec des événements supplémentaires, est une chose parfaitement raisonnable à faire.

VoiceOfUnreason
la source
Tout cela semble raisonnable. Lorsque j'ai joué avec une implémentation factice d'ES il y a quelque temps, j'ai utilisé EventStore pour garantir la cohérence, mais j'ai également écrit de manière synchrone dans ce que vous appelez un "référentiel d'instantanés". Cela signifiait que l'état actuel était toujours prêt à lire sans avoir à rejouer les événements. Je soupçonnais que cela n'allait pas bien, mais comme c'était juste un exercice, cela ne me dérangeait pas.
MetaFight
6

Puisque vous ne spécifiez pas quel serait le but de la base de données "d'écriture", je suppose ici que vous voulez dire ceci: lors de l'enregistrement d'une nouvelle mise à jour dans un agrégat, au lieu de reconstruire l'agrégat à partir du magasin d'événements, vous le retirer de la base de données "écriture", valider la modification et émettre un événement.

Si c'est ce que vous voulez dire, cette stratégie créera une condition d'incohérence: si une nouvelle mise à jour se produit avant que la dernière n'ait eu la chance de figurer dans la base de données "d'écriture", la nouvelle mise à jour finira par être validée par rapport à des données obsolètes, émettant ainsi potentiellement un événement «impossible» (c'est-à-dire «non autorisé») et corrompant l'état du système.

Par exemple, considérons un exemple permanent de réservation de sièges dans un théâtre. Pour éviter une double réservation, vous devez vous assurer que le siège en cours de réservation n'est pas déjà occupé - c'est ce que vous appelez la «validation». Pour ce faire, vous stockez une liste des places déjà réservées dans la base de données "écriture". Ensuite, lorsqu'une demande de réservation arrive, vous vérifiez si le siège demandé est dans la liste, et sinon, émettez un événement "réservé", sinon répondez par un message d'erreur. Ensuite, vous exécutez un processus de projection, où vous écoutez les événements "réservés" et ajoutez les sièges réservés à la liste dans la base de données "écriture".

Normalement, le système fonctionnerait comme ceci:

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Projection process catches the event, adds seat #1 to the "already booked" list.
5. Another request to book seat #1.
6. Check in the list: the list contains seat #1
7. Respond with an error message.

Cependant, que se passe-t-il si les demandes arrivent trop rapidement et que l'étape 5 se produit avant l'étape 4?

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Another request to book seat #1.
5. Check in the list: the list is still empty.
6. Issue another "booked seat #1" event.

Vous avez maintenant deux événements pour réserver le même siège. L'état du système est corrompu.

Pour éviter que cela ne se produise, vous ne devez jamais valider les mises à jour par rapport à une projection. Pour valider une mise à jour, vous reconstruisez l'agrégat à partir du magasin d'événements, puis validez la mise à jour par rapport à celle-ci. Après cela, vous émettez un événement, mais utilisez la protection d'horodatage pour vous assurer qu'aucun nouvel événement n'a été émis depuis votre dernière lecture dans le magasin. Si cela échoue, réessayez simplement.

La reconstruction des agrégats à partir du magasin d'événements peut entraîner une baisse des performances. Pour atténuer ce problème, vous pouvez stocker des instantanés agrégés directement dans le flux d'événements, étiquetés avec l'ID de l'événement à partir duquel l'instantané a été créé. De cette façon, vous pouvez reconstruire l'agrégat en chargeant l'instantané le plus récent et en relisant uniquement les événements qui l'ont suivi, au lieu de toujours rejouer l'intégralité du flux d'événements depuis le début du temps.

Fyodor Soikin
la source
Merci pour votre réponse (et désolé d'avoir mis si longtemps à répondre). Ce que vous dites sur la validation par rapport à la base de données d'écriture n'est pas nécessairement vrai. Comme je l'ai mentionné dans un autre commentaire, dans un exemple d'implémentation ES avec laquelle je jouais, je me suis assuré de mettre à jour ma base de données d'écriture de manière synchrone (et de stocker le concurrencyId / timestamp). Cela m'a permis de détecter des violations de concurrence optimistes sans avoir à me préparer à partir d'EventStore. Certes, les écritures synchrones seules ne protègent pas contre la corruption des données, mais je faisais également des écritures à accès unique (à thread unique).
MetaFight
J'ai donc réglé mes problèmes de cohérence. Cependant, j'ai supposé que cela se faisait au détriment de l'évolutivité.
MetaFight
L'écriture synchrone dans la base de données d'écriture comporte toujours un risque de corruption: que se passe-t-il si votre écriture dans le magasin d'événements réussit, mais votre écriture dans la base de données d'écriture échoue?
Fyodor Soikin
1
Si la projection en lecture échoue, elle réessayera jusqu'à ce qu'elle réussisse. S'il tombe en panne, il se réveillera et continuera d'où il s'est écrasé - en d'autres termes, réessayez également. L'effet observable à l'extérieur ne serait pas différent de celui qui fonctionne juste un peu lentement. Si la projection continue à échouer et à échouer de manière cohérente, cela signifierait qu'il y a un bogue dedans, et cela devra être corrigé. Après réparation, il reprendra son fonctionnement à partir du dernier bon état. Si toute la base de données lue est corrompue à la suite du bogue, je reconstruirai simplement la base de données à partir de zéro en utilisant l'historique des événements.
Fyodor Soikin
1
Aucune donnée n'est jamais perdue, c'est le gros point. Les données peuvent être bloquées dans une forme gênante (pour la lecture) pendant un certain temps, mais elles ne sont jamais perdues.
Fyodor Soikin
3

La raison principale est la performance. Vous pouvez stocker un instantané pour chaque validation (commit = les événements générés par une seule commande, généralement un seul événement), mais cela coûte cher. Le long de l'instantané, vous devez également stocker la validation, sinon ce ne serait pas le sourçage d'événements. Et tout cela doit se faire atomiquement, tout ou rien. Votre question n'est valide que si des bases de données / tables / collections distinctes sont utilisées (sinon ce serait exactement le sourçage d'événements), vous êtes donc obligé d'utiliser des transactions afin de garantir la cohérence. Les transactions ne sont pas évolutives. Un flux d'événements à ajouter uniquement (le magasin d'événements) est la mère de l'évolutivité.

La deuxième raison est l'encapsulation agrégée. Vous devez le protéger. Cela signifie que l'agrégat devrait être libre de modifier sa représentation interne à tout moment. Si vous le stockez et en dépendez beaucoup, vous aurez beaucoup de mal avec le versioning, ce qui arrivera à coup sûr. Dans le cas où vous utilisez l'instantané uniquement comme optimisation, lorsque le schéma change, vous ignorez simplement ces instantanés ( simplement ? Je ne le pense pas vraiment; bonne chance pour déterminer que le schéma d'Aggregate change - y compris toutes les entités imbriquées et les objets de valeur - dans un manière efficace et gérer cela).

Constantin Galbenu
la source
Lorsque mon schéma d'agrégation change, ne serait-il pas simple de rejouer mes événements pour générer une base de données "d'écriture" mise à jour?
MetaFight
Le problème est de détecter ce changement. Un agrégat peut être très volumineux, avec de nombreux fichiers / classes.
Constantin Galbenu
Je ne comprends pas. Le changement se produirait avec une version logicielle. La version viendrait probablement avec un script de base de données pour régénérer la base de données "écriture".
MetaFight
C'est beaucoup de travail à faire pour un script de migration. Pendant son exécution, l'application doit être arrêtée.
Constantin Galbenu
@MetaFight si le flux est très volumineux, il faudra beaucoup de temps pour reconstruire le nouveau schéma d'agrégat ... Je pense maintenant à un instantané qui est un état d'une projection en direct qui pourrait s'exécuter avant la sortie du nouvel agrégat schéma
Narvalex