CQRS Event Sourcing: valider le caractère unique du nom D'utilisateur

prenons un exemple simple d '"enregistrement de Compte", voici le flux:

  • visite de l'Utilisateur sur le site web
  • cliquez sur le bouton "Enregistrer" et remplissez le formulaire, cliquez sur le bouton" Enregistrer
  • MVC Controller: Validate UserName uniqueness by readmodel
  • RegisterCommand: Valider le nom d'utilisateur de l'unicité de nouveau (là est la question)

Of bien sûr, nous pouvons valider l'unicité du nom D'Utilisateur en lisant depuis ReadModel dans le contrôleur MVC pour améliorer les performances et l'expérience utilisateur. Cependant, nous avons encore besoin de valider l'unicité à nouveau dans RegisterCommand , et évidemment, nous ne devrions pas accéder au ReadModel dans les commandes.

Si nous n'utilisons pas d'Event Sourcing, nous pouvons interroger le modèle de domaine, donc ce n'est pas un problème. Mais si nous utilisons L'Event Sourcing, nous ne sommes pas en mesure d'interroger le modèle de domaine, donc comment valider l'unicité du nom D'utilisateur dans RegisterCommand?

avis: la classe D'utilisateur possède une propriété Id, et le nom D'utilisateur n'est pas la propriété clé de la classe D'utilisateur. Nous ne pouvons obtenir L'objet de domaine par Id qu'en utilisant event sourcing.

BTW: dans l'exigence, si le nom d'utilisateur entré est déjà pris, le site Web doit afficher le message d'erreur "Désolé, le nom D'utilisateur XXX n'est pas disponible" pour le visiteur. Il n'est pas acceptable de montrer un message, par exemple, "nous créons votre compte, s'il vous plaît attendez, nous vous enverrons le résultat de l'inscription par courriel plus tard", au visiteur.

des idées? Merci beaucoup!

[mise à JOUR]

un exemple plus complexe:

exigence:

lors de la passation d'une commande, le système doit: Vérifiez l'historique de commande du client, s'il est un client de valeur (si le client a passé au moins 10 commandes par mois dans la dernière année, il est précieux), nous faisons 10% de rabais à la commande.

mise en Œuvre:

nous créons PlaceOrderCommand, et dans la commande, nous devons interroger l'historique de commande pour voir si le client est valable. Mais comment pouvons-nous faire? Nous ne devrions pas accéder au ReadModel en commande! Comme le dit Mikael , nous pouvons utiliser des commandes de compensation dans l'exemple d'enregistrement de compte, mais si nous utilisons aussi cela dans cet exemple de commande, ce serait trop complexe, et le code pourrait être trop difficile à maintenir.

55
demandé sur Community 2012-02-29 12:47:36

7 réponses

si vous validez le nom d'utilisateur en utilisant le modèle read avant d'envoyer la commande, nous parlons d'une fenêtre de condition de course de quelques centaines de millisecondes où une vraie condition de course peut se produire, ce qui dans mon système n'est pas géré. Il est tout simplement trop peu probable que cela se produise par rapport au coût de son traitement.

Toutefois, si vous sentez que vous devez gérer cela pour une raison quelconque ou si vous vous sentez vous voulez savoir comment maîtriser un tel cas, voici une façon de:

vous ne devriez pas accéder au modèle de lecture à partir du gestionnaire de commandes ni au domaine lorsque vous utilisez event sourcing. Cependant, ce que vous pouvez faire est d'utiliser un service de domaine qui écouterait L'événement UserRegistered dans lequel vous accédez de nouveau au modèle de lecture et de vérifier si le nom d'utilisateur n'est toujours pas un duplicata. Bien sûr, vous devez utiliser L'UserGuid ici ainsi que votre modèle de lecture pourrait avoir été mis à jour avec l'utilisateur que vous venez de créer. S'il y a un duplicata trouvé, vous avez le possibilité d'envoyer des commandes de compensation telles que changer le nom d'utilisateur et informer l'utilisateur que le nom d'Utilisateur a été saisi.

C'est une approche du problème.

Comme vous pouvez probablement voir, il n'est pas possible de le faire en mode synchrone requête-réponse. Pour résoudre cela, nous utilisons SignalR pour mettre à jour L'interface utilisateur chaque fois qu'il y a quelque chose que nous voulons pousser vers le client (s'ils sont toujours connectés, c'est-à-dire). Ce que nous faisons, c'est laisser le web client abonnez-vous à des événements qui contiennent des renseignements utiles pour le client.

mise à Jour

pour le cas plus complexe:

je dirais que la commande est moins complexe, puisque vous pouvez utiliser le modèle de lire pour savoir si le client est précieux avant de vous envoyer la commande. En fait, vous pouvez interroger que lorsque vous chargez le formulaire de commande puisque vous voulez probablement montrer au client qu'ils obtiendront 10% de réduction avant de passer la commande. Il suffit d'ajouter un rabais à la PlaceOrderCommand et peut-être une raison de la réduction, de sorte que vous pouvez suivre pourquoi vous réduisez les bénéfices.

mais encore une fois, si vous avez vraiment besoin de calculer la réduction après la commande a été lieux pour une raison quelconque, encore une fois utiliser un service de domaine qui écouterait OrderPlacedEvent et la commande "compensation" dans ce cas-ci serait probablement un DiscountOrderCommand ou quelque chose. Cette commande affecterait la racine D'agrégat D'ordre et l'information pourrait être propagée à vos modèles de lecture.

pour le nom d'utilisateur en double:

vous pouvez envoyer un ChangeUsernameCommand comme commande de compensation du service de domaine. Ou même quelque chose de plus spécifique, qui décrirait la raison pour laquelle le nom d'Utilisateur a changé ce qui pourrait également entraîner la création d'un événement auquel le client web pourrait souscrire pour que vous puissiez montrer à l'utilisateur que le nom d'utilisateur était un duplicata.

dans le contexte du service de domaine, je dirais que vous avez également la possibilité d'utiliser d'autres moyens pour notifier l'utilisateur, comme l'envoi d'un e-mail qui pourrait être utile puisque vous ne pouvez pas savoir si l'utilisateur est toujours connecté. Peut-être que cette fonctionnalité de notification pourrait être initiée par le même événement auquel le client Web est abonné.

quand il s'agit de SignalR, j'utilise un Hub SignalR auquel les utilisateurs se connectent quand ils chargent une certaine forme. J'utilise la fonctionnalité de groupe SignalR qui me permet de créer un groupe dont je nomme la valeur du Guid que j'envoie dans la commande. Cela pourrait être le userGuid dans votre cas. Ensuite J'ai Eventhandler qui s'abonne à des événements qui pourraient être utiles pour le client et quand un événement arrive je peux invoquer une fonction javascript sur tous les clients du groupe SignalR (qui dans ce cas ne serait que le seul client à créer le nom d'utilisateur dupliqué dans votre cas). Je sais que ça a l'air complexe, mais ce n'est pas le cas. J'ai tout préparé en un après-midi. Il y a de grands docs et des exemples sur la page SignalR GitHub.

36
répondu Mikael Östberg 2012-02-29 12:46:51

je pense que vous devez encore avoir l'état d'esprit de cohérence éventuelle et la nature de l'approvisionnement d'événements. J'ai eu le même problème. En particulier, j'ai refusé d'accepter que vous devez faire confiance aux commandes du client qui, en utilisant votre exemple, disent "passer cette commande avec 10% de réduction" sans que le domaine validant que la réduction devrait aller de l'avant. Une chose qui a vraiment frappé la maison pour moi était quelque chose que Udi lui-même m'a dit (vérifier le commentaires de la accepté de répondre).

au fond, je me suis rendu compte qu'il n'y a aucune raison de ne pas faire confiance au client; tout ce qui est lu a été produit à partir du modèle de domaine, donc il n'y a aucune raison de ne pas accepter les commandes. Tout ce qui dans le côté lu qui dit que le client remplit les conditions pour le rabais a été mis là par le domaine.

BTW: dans l'exigence, si le nom d'utilisateur entré est déjà pris, le site web devrait afficher message d'erreur "Désolé, le nom d'utilisateur XXX n'est pas disponible" pour le visiteur. Il n'est pas acceptable de montrer un message, par exemple, "nous créons votre compte, s'il vous plaît attendez, nous vous enverrons le résultat de l'inscription par courriel plus tard", au visiteur.

si vous voulez adopter event sourcing & eventual consistency, vous devrez accepter que parfois il ne sera pas possible d'Afficher des messages d'erreur instantanément après avoir soumis une commande. Avec le nom d'utilisateur unique exemple les chances que cela se produise sont si minces (étant donné que vous vérifiez le côté lecture avant d'envoyer la commande) que cela ne vaut pas la peine de s'inquiéter de trop, mais une notification ultérieure devrait être envoyée pour ce scénario, ou peut-être leur demander un autre nom d'utilisateur la prochaine fois qu'ils se connectent. La grande chose au sujet de ces scénarios est qu'il vous amène à penser à la valeur de l'entreprise et ce qui est vraiment important.

mise à jour: Octobre 2015

je voulais juste ajouter, qu'en fait, lorsque le public fait face à des sites Web sont concernés - indiquant qu'un e-mail est déjà pris est en fait contre les meilleures pratiques de sécurité. Au lieu de cela, l'enregistrement devrait sembler avoir réussi à informer l'utilisateur qu'un email de vérification a été envoyé, mais dans le cas où le nom d'utilisateur existe, l'email devrait les informer de cela et les inviter à se connecter ou réinitialiser leur mot de passe. Bien que cela ne fonctionne que lorsque vous utilisez le courrier électronique adresses comme nom d'utilisateur, qui je pense est souhaitable pour cette raison.

19
répondu David Masters 2017-05-23 12:03:05

il n'y a rien de mal à créer des modèles de lecture immédiatement cohérents (par exemple pas sur un réseau distribué) qui sont mis à jour dans la même transaction que la commande.

avoir des modèles de lecture être éventuellement cohérent sur un réseau distribué aide à soutenir l'échelle du modèle de lecture pour les systèmes de lecture lourds. Mais il n'y a rien à dire que vous ne pouvez pas avoir un modèle de lecture de domaine spécifique thats immédiatement cohérent.

Le immédiatement le modèle de lecture cohérent n'est jamais utilisé que pour vérifier et recevoir des données avant d'émettre une commande (en fait c'est un service à la commande), vous ne devriez jamais l'utiliser pour afficher directement des données de lecture à un utilisateur (c.-à-d. à partir d'une demande GET web ou similaire). Utilisez éventuellement consitent, models de lecture extensible pour cela.

11
répondu Gaz_Edge 2015-09-24 13:35:15

comme beaucoup d'autres lors de la mise en œuvre d'un système basé sur un événement, nous avons rencontré le problème d'unicité.

au début, j'ai été un partisan de laisser le client accéder à la requête avant d'envoyer une commande afin de savoir si un nom d'utilisateur est unique ou non. Mais je me suis rendu compte qu'avoir un back-end qui n'a aucune validation sur l'unicité est une mauvaise idée. Pourquoi imposer quoi que ce soit quand il est possible d'afficher un ordre qui corromprait le système ? Un le back-end devrait valider toutes les entrées sinon vous êtes ouvert pour les données incohérentes.

nous avons créé un index à la commande. Par exemple, dans le cas simple d'un nom d'utilisateur qui doit être unique, il suffit de créer un UserIndex avec un champ username. Maintenant, le côté commande peut vérifier si un nom d'utilisateur est déjà dans le système ou pas. Après la commande a été exécutée, il est plus sûr de stocker le nouveau nom d'utilisateur dans l'index.

quelque chose comme ça pourrait également fonctionner pour le problème de réduction de commande.

les avantages sont que votre commande back-end valide correctement toutes les entrées afin qu'aucune donnée incohérente ne puisse être stockée.

un inconvénient pourrait être que vous avez besoin d'une requête supplémentaire pour chaque contrainte d'unicité et vous appliquez une complexité supplémentaire.

5
répondu Jonas Geiregat 2017-05-31 12:41:53

je pense que pour de tels cas, nous pouvons utiliser un mécanisme comme"Serrure Avec expiration".

Exemple d'exécution:

  • vérifier que le nom d'utilisateur existe ou non dans le modèle de lecture éventuellement cohérent
  • si elle n'existe pas; en utilisant une base de données redis-couchbase comme keyvalue storage ou cache; essayer de pousser le nom d'utilisateur comme champ de clé avec une certaine expiration.
  • en cas de succès; puis soulever userRegisteredEvent.
  • si un nom d'utilisateur existe dans le stockage en lecture ou en cache, informez le visiteur que le nom d'Utilisateur a été saisi.

même vous pouvez utiliser une base de données sql; insérez le nom d'utilisateur comme une clé primaire d'une table de verrouillage; et puis une tâche programmée peut gérer les expirations.

4
répondu Safak Ulusoy 2015-07-14 10:23:52

avez-vous envisagé d'utiliser un cache" fonctionnel " comme une sorte de RSVP? C'est difficile à expliquer parce que cela fonctionne dans un peu d'un cycle, mais fondamentalement, quand un nouveau nom d'utilisateur est "revendiqué" (c'est-à-dire que la commande a été émise pour le créer), vous placez le nom d'utilisateur dans le cache avec une courte expiration (assez longtemps pour tenir compte d'une autre requête passant par la file d'attente et dénormalisée dans le modèle de lecture). Si c'est une instance de service, alors dans la mémoire fonctionnerait probablement, sinon centraliser avec Redis ou quelque chose comme ça.

alors pendant que l'utilisateur suivant remplit le formulaire (en supposant qu'il y ait une face avant), vous vérifiez de façon asynchrone le modèle de lecture pour la disponibilité du nom d'utilisateur et alertez l'utilisateur s'il est déjà pris. Lorsque la commande est soumise, vous vérifiez le cache (pas le modèle de lecture) afin de valider la requête avant d'accepter la commande (avant de retourner 202); si le nom est dans le cache, n'acceptez pas la commande, si elle n'est pas alors vous l'ajoutez au cache; si l'ajout échoue (double clé parce qu'un autre processus vous a devancé), alors supposez que le nom est pris -- alors répondez au client de façon appropriée. Entre les deux choses, je ne pense pas qu'il y ait beaucoup d'opportunité pour une collision.

s'il n'y a pas d'extrémité avant, alors vous pouvez sauter la recherche asynchrone ou au moins demander à votre API de fournir le point final pour la recherche. Vous ne devriez vraiment pas permettre au client de parler directement au modèle de commande de toute façon, et placer une API devant, cela vous permettrait d'avoir L'API pour jouer le rôle de médiateur entre la commande et les hôtes de lecture.

1
répondu Sinaesthetic 2017-07-30 19:32:18

au sujet de l'unicité, j'ai mis en œuvre ce qui suit:

  • une première commande comme "StartUserRegistration". UserAggregate serait créé peu importe si l'utilisateur est unique ou non, mais avec un statut D'enregistrement requis.

  • Sur "UserRegistrationStarted" un message asynchrone serait envoyé à un apatride, de service "UsernamesRegistry". ce serait quelque chose comme "RegisterName".

  • le Service essaierait de mettre à jour (pas de requêtes, "tell don't ask") la table qui inclurait une contrainte unique.

  • en cas de succès, le service répondrait avec un autre message (asynchrone), avec une sorte d'autorisation "UsernameRegistration", indiquant que le nom d'Utilisateur a été enregistré avec succès. Vous pouvez inclure quelques iddemande à suivre en cas de compétence concurrente (peu probable).

  • l'émetteur du message ci-dessus a maintenant une autorisation que le nom a été enregistré par lui-même afin de pouvoir maintenant en toute sécurité marquer l'agrégat Userregistrement comme réussie. Sinon, marquez comme jeté.

Emballage:

  • cette approche n'implique aucune interrogation.

  • l'enregistrement de L'utilisateur serait toujours créé sans validation.

  • le processus de confirmation comporte deux messages asynchrones et une insertion db. La table ne fait pas partie d'un modèle de lire, mais d'un service.

  • Enfin, une commande asynchrone pour confirmer que l'Utilisateur est valide.

  • à ce stade, un dénormaliseur pourrait réagir à un événement confirmé par L'enregistrement D'un utilisateur et créer un modèle de lecture pour l'utilisateur.

0
répondu Daniel Vasquez 2018-08-30 00:48:09