Partager via


Concevoir la couche de persistance de l’infrastructure

Conseil / Astuce

Ce contenu est un extrait du livre électronique 'Architecture des microservices .NET pour les applications .NET conteneurisées', disponible sur .NET Docs ou en tant que PDF téléchargeable gratuitement, lisible hors ligne.

Architecture de microservices .NET pour les applications .NET conteneurisées - vignette de couverture du livre électronique.

Les composants de persistance des données fournissent un accès aux données hébergées dans les limites d’un microservice (autrement dit, la base de données d’un microservice). Ils contiennent l’implémentation réelle de composants tels que des référentiels et des classes d’unité de travail , comme des objets Entity Framework (EF) DbContext personnalisés. EF DbContext implémente à la fois le référentiel et les modèles d’unité de travail.

Modèle de référentiel

Le modèle référentiel est un modèle de conception Domain-Driven destiné à maintenir les problèmes de persistance en dehors du modèle de domaine du système. Une ou plusieurs abstractions de persistance ( interfaces) sont définies dans le modèle de domaine, et ces abstractions ont des implémentations sous la forme d’adaptateurs spécifiques à la persistance définis ailleurs dans l’application.

Les implémentations de référentiel sont des classes qui encapsulent la logique requise pour accéder aux sources de données. Ils centralisent les fonctionnalités courantes d’accès aux données, offrant une meilleure facilité de maintenance et découplant l’infrastructure ou la technologie utilisée pour accéder aux bases de données à partir du modèle de domaine. Si vous utilisez un mappeur Object-Relational (ORM) comme Entity Framework, le code à implémenter est simplifié, grâce à LINQ et à la saisie forte. Cela vous permet de vous concentrer sur la logique de persistance des données plutôt que sur la plomberie de l’accès aux données.

Le modèle référentiel est un moyen bien documenté d’utiliser une source de données. Dans le livre Patterns of Enterprise Application Architecture, Martin Fowler décrit un référentiel comme suit :

Un référentiel effectue les tâches d’un intermédiaire entre les couches de modèle de domaine et le mappage de données, agissant de la même façon qu’un ensemble d’objets de domaine en mémoire. Les objets clients créent de manière déclarative des requêtes et les envoient aux référentiels pour obtenir des réponses. Conceptuellement, un référentiel encapsule un ensemble d’objets stockés dans la base de données et les opérations qui peuvent être effectuées sur eux, fournissant un moyen plus proche de la couche de persistance. Les référentiels prennent également en charge l’objectif de séparer, clairement et dans une direction, la dépendance entre le domaine de travail et l’allocation ou le mappage des données.

Définir un référentiel par agrégat

Pour chaque agrégat ou racine d’agrégation, vous devez créer une classe de référentiel. Vous pouvez peut-être tirer parti des génériques C# pour réduire le nombre total de classes concrètes que vous devez conserver (comme illustré plus loin dans ce chapitre). Dans un microservice basé sur des modèles de conception (DDD) Domain-Driven, le seul canal que vous devez utiliser pour mettre à jour la base de données doit être les référentiels. Cela est dû au fait qu’ils ont une relation un-à-un avec la racine d’agrégation, qui contrôle les invariants et la cohérence transactionnelle de l’agrégat. Il est possible d’interroger la base de données via d’autres canaux (comme vous pouvez le faire en suivant une approche CQRS), car les requêtes ne modifient pas l’état de la base de données. Toutefois, la zone transactionnelle (autrement dit, les mises à jour) doit toujours être contrôlée par les référentiels et les racines d’agrégation.

En fait, un référentiel vous permet de remplir des données en mémoire provenant de la base de données sous la forme des entités de domaine. Une fois que les entités sont en mémoire, elles peuvent être modifiées, puis conservées dans la base de données par le biais de transactions.

Comme indiqué précédemment, si vous utilisez le modèle architectural CQS/CQRS, les requêtes initiales sont effectuées par des requêtes secondaires externes au modèle de domaine, réalisées par des instructions SQL simples à l’aide de Dapper. Cette approche est beaucoup plus flexible que les référentiels, car vous pouvez interroger et joindre les tables dont vous avez besoin, et ces requêtes ne sont pas limitées par les règles des agrégats. Ces données sont envoyées à la couche de présentation ou à l’application cliente.

Si l’utilisateur apporte des modifications, les données à mettre à jour proviennent de l’application cliente ou de la couche de présentation vers la couche application (par exemple, un service d’API web). Lorsque vous recevez une commande dans un gestionnaire de commandes, vous utilisez des référentiels pour obtenir les données que vous souhaitez mettre à jour à partir de la base de données. Vous la mettez à jour en mémoire avec les données transmises avec les commandes, puis vous ajoutez ou mettez à jour les données (entités de domaine) dans la base de données via une transaction.

Il est important de souligner à nouveau que vous ne devez définir qu’un dépôt pour chaque racine d’agrégation, comme illustré dans la figure 7-17. Pour atteindre l’objectif de la racine d’agrégation afin de maintenir la cohérence transactionnelle entre tous les objets de l’agrégat, vous ne devez jamais créer de référentiel pour chaque table de la base de données.

Diagramme montrant les relations de domaine et d’autres infrastructures.

Figure 7-17. Relation entre les référentiels, les agrégats et les tables de base de données

Le diagramme ci-dessus montre les relations entre les couches Domaine et Infrastructure : Buyer Aggregate dépend de IBuyerRepository et Order Aggregate dépend des interfaces IOrderRepository, ces interfaces sont implémentées dans la couche Infrastructure par les référentiels correspondants qui dépendent d’UnitOfWork, également implémentés, qui accèdent aux tables du niveau Données.

Appliquer une seule racine d’agrégat par référentiel

Il peut être précieux d’implémenter la conception de votre dépôt de manière à appliquer la règle selon laquelle seules les racines d'agrégats doivent posséder des dépôts. Vous pouvez créer un type de référentiel générique ou de base qui limite le type d’entités qu’il utilise pour s’assurer qu’elles disposent de l’interface de IAggregateRoot marqueur.

Ainsi, chaque classe de référentiel implémentée au niveau de la couche d’infrastructure implémente son propre contrat ou interface, comme indiqué dans le code suivant :

namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
    public class OrderRepository : IOrderRepository
    {
      // ...
    }
}

Chaque interface de référentiel spécifique implémente l’interface IRepository générique :

public interface IOrderRepository : IRepository<Order>
{
    Order Add(Order order);
    // ...
}

Toutefois, un meilleur moyen d’appliquer le code à la convention que chaque référentiel est lié à un agrégat unique consiste à implémenter un type de référentiel générique. De cette façon, il est explicite que vous utilisez un référentiel pour cibler un agrégat spécifique. Cela peut être facilement effectué en implémentant une interface de base générique IRepository , comme dans le code suivant :

public interface IRepository<T> where T : IAggregateRoot
{
    //....
}

Le modèle référentiel facilite le test de votre logique d’application

Le modèle référentiel vous permet de tester facilement votre application avec des tests unitaires. N’oubliez pas que les tests unitaires testent uniquement votre code, et non votre infrastructure, afin que les abstractions de référentiel facilitent l’atteinte de cet objectif.

Comme indiqué dans une section précédente, il est recommandé de définir et de placer les interfaces de référentiel dans la couche de modèle de domaine afin que la couche application, telle que votre microservice d’API web, ne dépend pas directement de la couche d’infrastructure où vous avez implémenté les classes de référentiel réelles. En procédant ainsi et en utilisant l’injection de dépendances dans les contrôleurs de votre API web, vous pouvez implémenter des référentiels fictifs qui retournent des données fausses au lieu de données de la base de données. Cette approche découplée vous permet de créer et d’exécuter des tests unitaires qui concentrent la logique de votre application sans nécessiter de connectivité à la base de données.

Les connexions aux bases de données peuvent échouer et, plus important encore, exécuter des centaines de tests sur une base de données est incorrecte pour deux raisons. Tout d’abord, cela peut prendre beaucoup de temps en raison du grand nombre de tests. Deuxièmement, les enregistrements de base de données peuvent changer et affecter les résultats de vos tests, en particulier si vos tests s’exécutent en parallèle, afin qu’ils ne soient pas cohérents. Les tests unitaires peuvent généralement s’exécuter en parallèle ; les tests d’intégration peuvent ne pas prendre en charge l’exécution parallèle en fonction de leur implémentation. Le test sur la base de données n’est pas un test unitaire, mais un test d’intégration. Vous devez avoir de nombreux tests unitaires en cours d’exécution rapide, mais moins de tests d’intégration sur les bases de données.

En termes de séparation des préoccupations pour les tests unitaires, votre logique fonctionne sur les entités de domaine en mémoire. Il part du principe que la classe de référentiel a remis ces données. Une fois que votre logique modifie les entités de domaine, elle suppose que la classe de référentiel les stocke correctement. Le point important ici est de créer des tests unitaires sur votre modèle de domaine et sa logique de domaine. Les racines d’agrégation sont les principales limites de cohérence dans DDD.

Les référentiels implémentés dans eShopOnContainers s’appuient sur l’implémentation DbContext d’EF Core du référentiel et de l’unité de travail à l’aide de son suivi des modifications, de sorte qu’ils ne dupliquent pas cette fonctionnalité.

Différence entre le modèle Référentiel et le modèle de classe d’accès aux données hérité (classe DAL)

Un objet DAL classique effectue directement des opérations d’accès aux données et de persistance sur le stockage, souvent au niveau d’une seule table et d’une seule ligne. Les opérations CRUD simples implémentées avec un ensemble de classes DAL ne prennent souvent pas en charge les transactions (bien que cela ne soit pas toujours le cas). La plupart des approches des classes DAL font un usage minimal des abstractions, ce qui entraîne un couplage étroit entre les classes de la couche de logique métier (BLL) qui invoquent les objets DAL.

Lors de l’utilisation du référentiel, les détails d’implémentation de la persistance sont encapsulés loin du modèle de domaine. L’utilisation d’une abstraction offre une facilité d’extension du comportement par le biais de modèles tels que décorateurs ou proxys. Par exemple, des préoccupations croisées telles que la mise en cache, la journalisation et la gestion des erreurs peuvent toutes être appliquées à l’aide de ces modèles plutôt que de code codé en dur dans le code d’accès aux données lui-même. Il est également facile de prendre en charge plusieurs adaptateurs de référentiel qui peuvent être utilisés dans différents environnements, du développement local aux environnements intermédiaires partagés et en production.

Implémentation de l’unité de travail

Une unité de travail fait référence à une transaction unique qui implique plusieurs opérations d’insertion, de mise à jour ou de suppression. En termes simples, cela signifie que pour une action utilisateur spécifique, telle qu’une inscription sur un site web, toutes les opérations d’insertion, de mise à jour et de suppression sont gérées dans une seule transaction. C'est plus efficace que de traiter plusieurs opérations de base de données de manière plus verbeuse.

Ces opérations de persistance multiples sont effectuées ultérieurement dans une seule action lorsque votre code à partir des commandes de la couche Application l’exécute. La décision d’appliquer les modifications en mémoire au stockage de base de données réel est généralement basée sur le modèle Unité de travail. Dans EF, le modèle Unité de travail est implémenté par un DbContext et est exécuté lorsqu’un appel est effectué à SaveChanges.

Dans de nombreux cas, ce modèle ou cette méthode d’application des opérations sur le stockage peut augmenter les performances de l’application et réduire la possibilité d’incohérences. Elle réduit également le blocage des transactions dans les tables de base de données, car toutes les opérations prévues sont validées dans le cadre d’une transaction. Cela est plus efficace par rapport à l’exécution de nombreuses opérations isolées sur la base de données. Par conséquent, l’ORM sélectionné peut optimiser l’exécution sur la base de données en regroupant plusieurs actions de mise à jour au sein de la même transaction, par opposition à de nombreuses exécutions de transactions petites et distinctes.

Le modèle Unité de travail peut être implémenté avec ou sans utiliser le modèle référentiel.

Les référentiels ne doivent pas être obligatoires

Les référentiels personnalisés sont utiles pour les raisons mentionnées précédemment, et c’est l’approche du microservice de commande dans eShopOnContainers. Toutefois, il n’est pas un modèle essentiel à implémenter dans une conception DDD ou même dans le développement .NET général.

Par exemple, Jimmy Bogard, en fournissant des commentaires directs pour ce guide, a déclaré ce qui suit :

Ce sera probablement mon plus gros retour. Je ne suis vraiment pas un fan de référentiels, principalement parce qu’ils masquent les détails importants du mécanisme de persistance sous-jacent. C'est pourquoi j'opte aussi pour MediatR quand il s'agit des commandes. Je peux utiliser toute la puissance de la couche de persistance et intégrer toutes les fonctionnalités du domaine dans les racines de mes agrégats. Je ne veux généralement pas simuler mes référentiels : j’ai toujours besoin d’avoir ce test d’intégration avec la réalité. L’utilisation de CQRS signifiait que nous n’avions pas vraiment besoin de référentiels.

Les référentiels peuvent être utiles, mais ils ne sont pas essentiels pour votre conception DDD de la façon dont le modèle d’agrégation et un modèle de domaine enrichi sont. Par conséquent, utilisez le modèle Dépôt ou ne l’utilisez pas, en fonction de vos besoins.

Ressources supplémentaires

Modèle de référentiel

Modèle d’unité de travail