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?
Réponses:
Parce que les "événements" sont le livre des records.
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.
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.
la source
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:
Cependant, que se passe-t-il si les demandes arrivent trop rapidement et que l'étape 5 se produit avant l'étape 4?
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.
la source
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).
la source