Assurer la cohérence transactionnelle avec DDD

9

Je commence par DDD et je comprends que les racines agrégées sont utilisées pour assurer la cohérence transnationale. Nous ne devons pas modifier plusieurs agrégats dans un seul service d'application.

Je voudrais cependant savoir comment faire face à la situation suivante.

J'ai une racine agrégée appelée Produits.

Il existe également une racine agrégée appelée Group.

Les deux ont des identifiants et peuvent être modifiés indépendamment.

Plusieurs produits peuvent pointer vers le même groupe.

J'ai un service d'application qui peut changer le groupe d'un produit:

ProductService.ChangeProductGroup(string productId, string groupId)

  1. Vérifier que le groupe existe
  2. Obtenir le produit du référentiel
  3. Définir son groupe
  4. Réécrire le produit dans le référentiel

J'ai également un service d'application où le groupe peut être supprimé:

GroupService.DeleteGroup(string groupId) 1. Obtenez les produits du référentiel dont groupId est défini sur groupId fourni, assurez-vous que le nombre est 0 ou abandonnez 2. Supprimez le groupe du référentiel de groupes 3. Enregistrez les modifications

Ma question est le scénario suivant, que se passerait-il si:

Dans ProductService.ChangeProductGroup, nous vérifions que le groupe existe (il le fait), puis juste après cette vérification, un utilisateur distinct supprime le productGroup (via l'autre GroupService.DeleteGroup). Dans ce cas, nous avons défini une référence à un produit qui vient d'être supprimé?

Est-ce un défaut dans ma conception dans la mesure où je devrais utiliser une conception de domaine différente (en ajoutant des éléments supplémentaires si nécessaire), ou devrais-je utiliser des transactions?

morleyc
la source

Réponses:

2

Nous avons le même problème.

Et je ne vois aucun moyen de résoudre ce problème, mais avec des transactions ou avec un contrôle de cohérence au niveau de la base de données pour obtenir une exception dans le pire des cas.

Vous pouvez également utiliser un verrou pessimiste, bloquer la racine agrégée ou uniquement ses parties pour d'autres clients jusqu'à ce que la transaction commerciale soit terminée, ce qui équivaut dans une certaine mesure à une transaction sérialisable.

La façon dont vous procédez dépend fortement de votre système et de votre logique métier.
La concurrence n'est pas du tout une tâche facile.
Même si vous pouvez détecter un problème, comment allez-vous le résoudre? Annuler simplement l'opération ou autoriser l'utilisateur à «fusionner» les modifications?

Nous utilisons Entity Framework et EF6 utilise read_commited_snapshot par défaut, donc deux lectures consécutives à partir du référentiel peuvent nous donner des données incohérentes. Nous gardons cela à l'esprit pour l'avenir lorsque les processus commerciaux seront plus clairement définis et que nous pourrons prendre des décisions infromées. Et oui, nous vérifions toujours la cohérence au niveau du modèle comme vous le faites. Cela permet au moins de tester BL séparément de DB.

Je vous suggère également de réfléchir à la cohérence du référentiel au cas où vous auriez des transactions commerciales «longues». Il s'est avéré assez délicat.

Pavel Voronin
la source
1

Je pense que ce n'est pas une question spécifique à la conception pilotée par domaine, mais une question de gestion des ressources.

Les ressources doivent simplement être verrouillées lors de leur acquisition .

Cela est vrai lorsque le service d'application sait à l'avance que la ressource acquise (l' Groupagrégat, dans votre exemple) va être modifiée. Dans la conception pilotée par le domaine, le verrouillage des ressources est un aspect qui appartient au référentiel.

rucamzu
la source
Non, le verrouillage n'est pas un "must". En fait, c'est une chose assez horrible à faire avec des transactions de longue durée. Il vaut mieux utiliser une concurrence optimiste, que chaque ORM moderne prend en charge dès le départ. Et l'avantage de la concurrence optimiste est qu'elle fonctionne également avec les bases de données non transactionnelles, à condition qu'elles prennent en charge les mises à jour atomiques.
Aaronaught
1
Je suis d'accord sur les inconvénients du verrouillage à l'intérieur de transactions de longue durée, mais le scénario décrit par @ g18c laisse entendre tout à fait le contraire, c'est-à-dire de brèves opérations sur des agrégats individuels.
rucamzu
0

Pourquoi ne stockez-vous pas les ID de produit dans l'entité Groupe? De cette façon, vous n'avez affaire qu'à un seul agrégat, ce qui facilitera les choses.

Vous devrez ensuite implémenter un type de modèle de concurrence, par exemple. Si vous choisissez une concurrence optimiste, ajoutez simplement une propriété de version à l'entité Group et lancez une exception si les versions ne correspondent pas lors de la mise à jour.

Ajouter un produit à un groupe

  1. Vérifiez si le produit existe
  2. Récupérer le groupe du référentiel (y compris sa propriété id de version)
  3. Group.Add (ProductId)
  4. Enregistrer le groupe à l'aide du référentiel (mettre à jour avec le nouvel identifiant de version et ajouter une clause where pour garantir que la version n'a pas changé.)

Beaucoup d'ORM ont une concurrence optimiste intégrée en utilisant des identifiants de version ou des horodatages, mais il est facile de faire le vôtre. Voici un bon article sur la façon dont cela se fait à Nhibernate http://ayende.com/blog/3946/nhibernate-mapping-concurrency .

Scott Millett
la source
0

Bien que les transactions puissent aider, il existe un autre moyen, surtout si votre scénario se limite à quelques cas.

Vous pouvez inclure des vérifications dans les requêtes qui effectuent les mutations, ce qui fait de chaque paire de vérifications et mutations une opération atomique.

La requête interne de mise à jour du produit rejoint le groupe (nouvellement référencé). Cela le fait ne rien mettre à jour si ce groupe est parti.

La requête de suppression de groupe joint tous les produits pointant vers elle et a la condition supplémentaire que (n'importe quelle colonne de) le résultat de la jointure doit être nul. Cela lui permet de ne rien supprimer si des produits le désignent.

Les requêtes renvoient le nombre de lignes qui correspondent ou ont été mises à jour. Si le résultat est 0, vous avez perdu une condition de concurrence et n'avez rien fait. Il peut lever une exception et la couche application peut gérer cela comme bon lui semble, par exemple en réessayant depuis le début.

Timo
la source