Novembre 2016

Volume 31, numéro 11

Cet article a fait l'objet d'une traduction automatique.

Points de données : modèles de données CQRS et EF

Par Julie Lerman

Julie LermanCommandes requête répartition (CQRS) est un modèle qui fournit essentiellement des indications sur la séparation de la responsabilité de la lecture des données et entraîné un changement d’état du système (par exemple, l’envoi d’un message de confirmation ou l’écriture dans une base de données) et conception d’architecture et les objets en conséquence. Il a été initialement conçu pour vous aider dans les systèmes hautement transactionnels tels que des opérations bancaires. Greg Young évolué CQRS à partir de la stratégie de séparation des requêtes de commandes (CQS) de Bertrand Meyer dont idée plus important, en fonction de Martin Fowler, « est qu’il est très pratique si vous pouvez séparer clairement les méthodes qui modifient l’état de celles qui ne le font » (bit.ly/2cuoVeX). Ce qui ajoute la CQRS est l’idée de création de modèles totalement distincts pour les commandes et requêtes.

CQRS a souvent été placée dans des compartiments est incorrecte, comme un type particulier de l’architecture, ou dans le cadre de la conception ou comme la messagerie ou gestion des événements. Dans un billet de blog de 2010, « CQRS, interfaces utilisateur Office, événement approvisionnement, Agh ! » (bit.ly/1fZwJ0L), Young explique qu’aucune de ces choses, mais uniquement un modèle pour vous aider à prendre des décisions d’architecture CQRS. CQRS est réellement de « lorsque les deux objets où il a été précédemment qu’un seul. » Il n’est pas spécifique aux modèles de données ou les limites du service, mais il peut certainement être appliqué aux parties de votre logiciel. En fait, il indique que « le plus grand avantage possible cependant est qu’il reconnaît que leurs (sic) est différentes propriétés architecturales lors du traitement des commandes et requêtes.

Lorsque vous définissez des modèles de données (souvent avec Entity Framework [Entity FRAMEWORK]), je suis devenu un fan de l’utilisation de ce modèle, dans certains scénarios particuliers. Comme toujours, mes idées sont conçues comme des conseils, pas des règles, et comme j’ai choisi de CQRS d’une manière qui me permet d’atteindre mon architecture, j’espère que vous allez prendre les et mettre en forme les adapter à vos besoins.

Avantages de la relation de gestion avec Entity FRAMEWORK

Entity Framework permet de travailler avec des relations au moment du design si facile. Lors de l’interrogation, il s’agit d’un avantage énorme. Les relations qui existent entre des entités permettent de parcourir les relations pour exprimer des requêtes. Récupérer des données associées à partir de la base de données est simple et efficace. Vous pouvez choisir de le chargement de la méthode Include ou les projections, après avoir fait le chargement différé ou après avoir fait le chargement explicit hâtif. Ces fonctionnalités n’ont pas beaucoup changé depuis la version d’origine d’EF, ni dans la mesure où j’ai écrit à leur sujet en juin 2011, « stratégies de comprendre Entity Framework : Chargement de données connexes » (msdn.com/magazine/hh205756).

L’exemple canonique dans le modèle Figure 1 rend interrogation facile d’afficher les détails d’une commande client, les facturation et le produit noms sur une page. Vous pouvez écrire une requête efficace comme suit :

var customersWithOrders = context.Customers
  .Include(c => c.Orders.Select(
  o => o.LineItems.Select(p => p.Product)))
  .ToList();

Modèle de données Entity Framework avec des relations fortement couplées
Figure 1 modèle de données Entity Framework avec des relations fortement couplées

EF Cela transforme SQL qui permettra de récupérer toutes les données pertinentes en une seule commande de base de données. Ensuite, à partir des résultats, EF matérialisera les graphiques complètes de clients, leurs commandes, les éléments de ligne de commandes et même les détails du produit pour chaque ligne.

Il est certainement remplissant une page similaire à la fenêtre fenêtre Presentation Foundation (WPF) dans Figure 2 facile. Je peux le faire dans une seule ligne de code :

customerViewSource.Source = customersWithOrders

Contrôles de données liés à un graphique d’objet unique
Figure 2 contrôles de données liés à un graphique d’objet unique

Voici un autre avantage des développeurs : Lors de la création de graphiques, Entity FRAMEWORK fonctionne l’arrière et suite à la base de données pour insérer le parent, retourner la nouvelle valeur de clé primaire, puis appliquez que comme la valeur de clé étrangère aux enfants avant de générer et exécuter les commandes insert.

Il est assez magique. Mais magic a ses inconvénients, et dans le cas de modèles de données Entity FRAMEWORK, la magie provenant d’avoir étroitement liée de relations peut entraîner des effets secondaires lorsqu’il est temps pour effectuer des mises à jour et parfois même des requêtes. Un effet secondaire notable peut se produire lorsque vous attachez des données de référence à un nouvel enregistrement à l’aide d’une propriété de navigation, puis appeler SaveChanges. Par exemple, vous pouvez créer un nouvel élément de ligne et définir sa propriété de produit à une instance d’un produit existant provenant de la base de données. Dans une application connectée, telle qu’une application WPF, où Entity FRAMEWORK peut être suivi de chaque modification à ses objets, Entity FRAMEWORK obtient que le produit a été déjà existant. Mais dans des scénarios déconnectés où Entity FRAMEWORK commence à suivre les objets uniquement après que les modifications ont été apportées, Entity FRAMEWORK suppose que le produit, comme l’élément de ligne, est une nouveauté et s’insérer dans la base de données à nouveau. Il existe des solutions de contournement pour ces problèmes, bien sûr. Pour ce problème, je vous recommande toujours de définir la valeur de clé étrangère (ProductId) au lieu de l’instance. Il existe également des méthodes pour effectuer le suivi de l’état et dompter les choses avec Entity FRAMEWORK avant l’enregistrement des données. En fait, mon article récent, « Gérer l’état de déconnecté entités dans EF » (msdn.com/magazine/mt694083), présente un modèle correspondant.

Voici un autre piège courant : propriétés de navigation qui sont requises. Selon la façon dont vous interagissez avec un objet, vous pouvez s’intéressent pas à la propriété de navigation, mais Entity FRAMEWORK remarquerez certainement s’il est manquant. J’ai parlé de ce type de problème dans une autre colonne, « Rendre faire avec clé étrangère » (msdn.com/magazine/hh708747).

Donc, Oui, il existe des solutions de contournement. Mais vous pouvez également exploiter le modèle CQRS pour créer des API plus clair et plus explicites qui n’ont pas besoin de solutions de contournement. Cela signifie également qu’ils seront plus faciles à gérer et moins sujet aux effets secondaires supplémentaires.

Appliquer le modèle CQRS de DbContext et Classes de domaine

J’ai souvent utilisé le modèle CQRS pour m’aider à résoudre ce problème. Accordée que cela signifie que tout ce que les modèles vous êtes casser entraîne deux fois plus de classes (mais pas nécessairement deux fois plus de code). Non seulement créer deux DbContexts distincts, mais souvent je vais me retrouver avec des paires de classes de domaine, chacune axée sur les tâches autour de lecture ou d’écriture.

J’utiliserai mon exemple un modèle qui est légèrement différent de formulaire celui plus simple que j’ai déjà présenté. Cet exemple provient d’une solution dimensionnable, que j’ai créé un cours Pluralsight récente. Dans le modèle, il existe une classe de SalesOrder qui agit comme la racine d’agrégat dans le domaine. En d’autres termes, le type de SalesOrder contrôle ce qui se passe pour les autres types connexes de l’agrégat, il contrôle LineItems création, comment les remises sont calculées, comment une adresse d’expédition est dérivée et ainsi de suite. Si vous pensez que sur les tâches que je viens de mentionner, ils sont davantage centrées sur la création d’une commande. Vous n’avez vraiment à vous soucier des règles sur la création d’un nouvel élément de ligne de commande lorsque vous lisez simplement les informations de commande à partir de la base de données.

En revanche, lorsque vous affichez des données, il peut être beaucoup plus intéressant de voir que je m’intéresse lorsque je suis simplement transmettre des données dans la base de données d’informations.

Un modèle de données interrogées

Figure 3 affiche le type de commande client dans le projet Order.Read.Domain de ma solution. Il existe un grand nombre de propriétés ici et afficher les données qu’une seule méthode de création d’une meilleure. Vous ne voyez pas les règles d’entreprise ici, car je n’ai pas à vous soucier de la validation des données.

Lit le Type SalesOrder défini pour être utilisé pour les données de la figure 3

namespace Order.Read.Domain {
 public class SalesOrder : Entity  {
  protected SalesOrder()   {
    LineItems = new List<LineItem>();
  }
  public DateTime OrderDate { get; set; }
  public DateTime? DueDate { get; set; }
  public bool OnlineOrder { get; set; }
  public string PurchaseOrderNumber { get; set; }
  public string Comment { get; set; }
  public int PromotionId { get; set; }
  public Address ShippingAddress { get; set; }
  public CustomerStatus CurrentCustomerStatus { get; set; }
  public double Discount   {
    get { return CustomerDiscount + PromoDiscount; }
  }
  public double CustomerDiscount { get; set; }
  public double PromoDiscount { get; set; }
  public string SalesOrderNumber { get; set; }
  public int CustomerId { get; set; }
  public double SubTotal { get; set; }
  public ICollection<LineItem> LineItems { get; set; }
  public decimal CalculateShippingCost()   {
    // Items, quantity, price, discounts, total weight of item
    // This is the job of a microservice we can call out to
    throw new NotImplementedException();
  }
}

Comparez cela à la commande dans Figure 4, lequel j’ai défini pour les scénarios où je stockerai SalesOrder des données à la base de données, s’il s’agit d’une commande ou un que je suis en train de modifier. Il est beaucoup plus logique d’entreprise dans cette version. Il existe une méthode de fabrique avec un constructeur privé et protégé que vous assurer qu’une commande ne peut pas être créée sans données particulières soient disponibles. Il existe des méthodes avec une logique et des règles pour la façon dont un nouvel élément de ligne peuvent être créé pour une commande, ainsi que comment appliquer une adresse d’expédition. Il existe une méthode pour contrôler quand et comment un ensemble particulier de détails de la commande peut être modifié.

Figure 4 Type SalesOrder pour la création et la mise à jour des données

namespace Order.Write.Domain {
  public class SalesOrder : Entity   {
    private readonly Customer _customer;
    private readonly List<LineItem> _lineItems;
    public static SalesOrder Create(IEnumerable<CartItem>
      cartItems, Customer customer) {
      var order = new SalesOrder(cartItems, customer);
      return order;
    }
    private SalesOrder(IEnumerable<CartItem> cartItems, Customer customer) : this(){
      Id = Guid.NewGuid();
      _customer = customer;
      CustomerId = customer.CustomerId;
      SetShippingAddress(customer.PrimaryAddress);
      ApplyCustomerStatusDiscount();
      foreach (var item in cartItems)
      {
        CreateLineItem(item.ProductId, (double) item.Price, item.Quantity);
      }
      _customer = customer;
    }
    protected SalesOrder() {
      _lineItems = new List<LineItem>();
      Id = Guid.NewGuid();
      OrderDate = DateTime.Now;
    }
    public DateTime OrderDate { get; private set; }
    public DateTime? DueDate { get; private set; }
    public bool OnlineOrder { get; private set; }
    public string PurchaseOrderNumber { get; private set; }
    public string Comment { get; private set; }
    public int PromotionId { get; private set; }
    public Address ShippingAddress { get; private set; }
    public CustomerStatus CurrentCustomerStatus { get; private set; }
    public double Discount{
      get { return CustomerDiscount + PromoDiscount; }
    }
    public double CustomerDiscount { get; private set; }
    public double PromoDiscount { get; private set; }
    public string SalesOrderNumber { get; private set; }
    public int CustomerId { get; private set; }
    public double SubTotal { get; private set; }
    public ICollection<LineItem> LineItems  {
      get { return _lineItems; }
    }
    public void CreateLineItem(int productId, double listPrice, int quantity)
    {
      // NOTE: more rules to be implemented here
      var item = LineItem.Create(Id, productId, quantity, listPrice,
        CustomerDiscount + PromoDiscount);
      _lineItems.Add(item);
    }
    public void SetShippingAddress(Address address) {
      ShippingAddress = Address.Create(address.Street, address.City,
        address.StateProvince, address.PostalCode);
    }
    public bool HasLineItems(){
      return LineItems.Any();
    }
    public decimal CalculateShippingCost() {
      // Items, quantity, price, discounts, total weight of item
      // This is the job of a microservice we can call out to
      throw new NotImplementedException();
    }
    public void ApplyCustomerStatusDiscount() {
      // The guts of this method are in the sample
    }
    public void SetOrderDetails(bool onLineOrder,
      string PONumber, string comment, int promotionId, double promoDiscount){
      OnlineOrder = onLineOrder;
      PurchaseOrderNumber = PONumber;
      Comment = comment;
      PromotionId = promotionId;
      PromoDiscount = promoDiscount;
    }
  }
}

La version de l’écriture de SalesOrder est plus complexe. Mais si j’ai jamais besoin travailler sur la version de lecture, je n’ai pas toute cette logique écriture superflues dans mon moyen. Si vous êtes un fan des conseils que code lisible est du code qui est moins sujet aux erreurs, comme moi, peut-être encore une autre raison de préférer cette séparation. Et sûrement quelqu'un comme Young pourrait même cette classe comporte beaucoup trop logique. Mais nos fins, ce qui fera.

Le modèle CQRS me permet de vous concentrer sur les problèmes de remplissage SalesOrder (qui sont dans ce cas, peu) et les problèmes de création de SalesOrder séparément lors de la définition des classes. Ces classes possèdent certaines choses en commun. Par exemple, les deux versions de la classe SalesOrder définissent une relation pour le type d’élément de commande avec une propriété ICollection < liste >.

Maintenant examinons leurs modèles de données ; Autrement dit, les classes DbContext utiliser pour accéder aux données.

Le OrderReadContext définit un DbSet unique, ce qui concerne l’entité SalesOrder :

public DbSet<SalesOrder> Orders { get; set; }

Entity FRAMEWORK détecte le type d’élément de commande associés et génère le modèle illustré Figure 5. Toutefois, comme Entity FRAMEWORK requiert le DbSet exposées, il permet également de tous les appelants OrderReadContext.SaveChanges. C’est en couches sont votre ami. Andrea Saltarello fournit un excellent moyen d’encapsuler le DbContext afin que seuls le DbSet est exposé et les développeurs (ou à venir vous) à l’aide de cette classe n’ont pas accès direct à la OrderReadContext. Cela peut contribuer à éviter d’appeler accidentellement SaveChanges sur le modèle de lecture.

Le modèle de données basé sur le OrderReadContext
Figure 5 le modèle de données basé sur le OrderReadContext

Est un exemple simple de cette classe :

public class ReadModel {
  private OrderReadContext readContext = null;
  public ReadModel() {
    readContext = new OrderReadContext();
  }
  public IQueryable<SalesOrder> Orders {
    get {
      return readContext.Orders;
    }
  }
}

Protection un autre, que vous pouvez ajouter à cette implémentation consiste à tirer parti du fait que SaveChanges est virtuel. Vous pouvez remplacer SaveChanges afin qu’il n’appelle jamais la méthode DbContext.SaveChanges interne.

Le OrderWriteContext définit deux propriétés DbSet : plutôt qu’un seul SalesOrder, mais un autre pour l’entité de l’élément de commande :

public DbSet<SalesOrder> Orders { get; set; }
public DbSet<LineItem> LineItems { get; set; }

Déjà qui est intéressant, car je n’a pas pris la peine exposer un DbSet pour les éléments de commande dans l’autres DbContext. Dans la OrderReadContext, allez interroger uniquement via le SalesOrders. Ne jamais interroger directement sur les éléments de commande, donc il y a pas besoin d’exposer un DbSet pour ce type. N’oubliez pas de la requête pour remplir la fenêtre WPF comme dans Figure 2. J’ai chargement hâtif les éléments de commande via le DbSet Orders.

La logique dans le OrderWriteContext autres importante est que j’ai dit explicitement EF d’ignorer la relation entre SalesOrder et LineItem à l’aide de l’API fluent :

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
  modelBuilder.Entity<SalesOrder>().Ignore(s => s.LineItems);
}

Le modèle résultant ressemblera à Figure 6.

Le modèle de données basé sur le OrderWriteContext
Figure 6 le modèle de données basé sur le OrderWriteContext

Cela signifie que pour naviguer entre SalesOrder LineItem Impossible d’utiliser Entity FRAMEWORK. Il n’empêche pas me faisant dans ma logique métier ; comme vous l’avez vu, j’ai beaucoup de code dans la classe SalesOrder qui interagit avec les éléments de commande. Mais je ne sera pas en mesure d’écrire une requête qui parcourt pour les éléments de commande, comme contexte. SalesOrders.Include (s = > s.LineItems). Qui peut déclencher un moment de panique jusqu'à ce que je vous rappelle qu’il s’agit du modèle pour l’écriture de données, mais ne pas pour lire. Entity FRAMEWORK peut récupérer des données associées sans problème à l’aide de la OrderReadContext.

Avantages et inconvénients d’un DbContext exempt de relation pour les écritures

Par conséquent, ce qui ai acquis par la séparation des responsabilités de l’écriture à partir de l’interrogation responsabilités ? Il est facile de voir les inconvénients. J’ai plus de code pour mettre à jour. Plus important encore, EF pas comme par magie mise à jour des graphiques pour moi. Je dois effectuer plus de travail manuellement pour garantir que lorsque je suis insertion, mise à jour ou supprimer des données, les relations sont gérées correctement. Par exemple, si vous avez un code qui ajoute un nouvel article dans une commande client, la simple écriture myOrder.LineItems.Add(someItem) ne déclenchent pas EF pour pousser l’orderId dans l’élément de commande lorsqu’il est temps pour conserver l’élément de commande dans la base de données. Vous devez définir explicitement cette valeur orderId. Si vous revenez à la méthode CreateLineItem de la commande dans Figure 4, vous verrez que j’ai couvertes. Dans mon système, la seule façon de créer un nouvel élément de ligne de commande est via cette méthode très, ce qui signifie que je ne parviens pas à écrire du code ailleurs absences cette étape essentielle de l’application de la colonne orderId. Une autre question que vous vous demandez peut-être est : « Que se passe-t-il si je souhaite modifier la colonne orderId d’une ligne particulière ? » Dans mon système, qui est une action qui ne fait pas beaucoup de sens. Je peux voir supprimer des éléments de ligne de commandes. Je peux voir Ajout d’éléments de ligne de commandes. Mais il n’existe aucune règle d’entreprise qui permet de changer l’orderId. Cependant, je ne peux pas vous aider pensée de ces « quelles ifs », car je suis habitué donc à la construction ces fonctionnalités dans mon modèle de données.

Outre le contrôle explicit, que j’ai sur les relations, dernières la logique de lecture et écriture obtient également me penser à la logique de toutes les qu'ajouter à mes données modèles par défaut, lorsqu’une partie de logique ne sera jamais utilisée. Et cette logique superflue peut forcer à écrire des solutions de contournement pour éviter de ses effets.

Les problèmes que je soulevés précédemment sur les données de référence de nouveau ajoutées à la base de données par accident ou introduit lorsque vous lisez des données que vous ne souhaitez pas mettre à jour les valeurs null, ces problèmes disparaissent également. Une classe définie pour la lecture peut-être inclure des valeurs que vous souhaitez afficher, mais pas à jour. Mon exemple SalesOrder n’a pas ce problème particulier. Mais une classe d’écriture peut éviter notamment les propriétés que vous souhaiterez peut-être afficher mais pas mettre à jour et, par conséquent, éviter d’écraser ignoré propriétés avec des valeurs null.

Assurez-vous que cela vaut la peine

CQRS peut ajouter beaucoup de travail pour le développement de votre système. N’oubliez pas de jeter un œil aux articles qui fournissent des conseils sur lorsque CQRS peut s’avérer superflu pour le problème que vous êtes résoudre, tel que celui par Udi Dahan à bit.ly/2bIbd7i. « CQRS pour l’Application courante » de Dino Esposito (msdn.com/magazine/mt147237) fournit également un aperçu. Mon utilisation particulière de ce modèle n’est pas ce que vous pouvez considérer comme CQRS complet, mais étant donné « autorisation » pour diviser les lectures et écritures par CQRS a m’a aidé à réduire la complexité des solutions où un modèle de données overreaching a été mise en route de la façon. Trouver un équilibre entre l’écriture de code supplémentaire pour éviter des effets secondaires ou écrire du code supplémentaire pour fournir plus propre, des chemins d’accès plus directes pour résoudre le problème est une expérience et la confiance. Mais parfois, votre instinct est plus fiable.


Julie Lerman est MVP Microsoft, mentor et conseillère .NET. Elle habite dans les collines du Vermont. Vous pouvez trouver sa présentation sur l’accès aux données et d’autres rubriques .NET à des groupes d’utilisateurs et des conférences dans le monde entier. À l’adresse thedatafarm.com et est l’auteur de « Programming Entity Framework », ainsi qu’un Code First et une édition DbContext, à partir d’o ' Reilly Media. Vous pouvez la suivre sur Twitter : @julielerman et consulter ses cours sur Pluralsight sur juliel.me/PS-Videos.

Merci à l'expert technique suivant d'avoir relu cet article : Andrea Saltarello (conceptions gérées) (andrea.saltarello@manageddesigns.it)
Andrea Saltarello est architecte entrepreneur et les logiciels de Milan, Italie, qui a toujours adore écrire du code des projets réels pour obtenir des commentaires sur ses décisions de conception. En tant que formateur et intervenant, il a plusieurs engagements rémunérés pour les cours et des conférences en Europe, tels que TechEd Europe, DevWeek et architecte logiciel. Il est MVP Microsoft depuis 2003 et a été récemment été nommé directeur régional Microsoft. Il est passionné de musique et est consacré au Depeche Mode, avec lequel il a été amoureux depuis écoute « Tout compte » pour la première fois.