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

Points de données

Réduire les modèles EF avec des contextes associés DDD

Julie Lerman

Télécharger l'exemple de code

Julie LermanLorsque vous définissez des modèles pour une utilisation avec l'Entity Framework (EF), les développeurs incluent souvent toutes les classes à utiliser dans toute l'application. Cela pourrait être une conséquence de la création d'un nouveau modèle de la première base de données dans le concepteur de l'EF et sélectionner toutes les tables disponibles et les vues de la base de données. Pour ceux d'entre vous à l'aide de Code First pour définir votre modèle, il peut signifier création DbSet propriétés dans un seul DbContext pour toutes vos classes ou même sans le savoir, y compris les classes qui sont liés à ceux que vous avez ciblés.

Lorsque vous travaillez avec un grand modèle et une application volumineuse, il y a plusieurs avantages à concevoir des modèles plus petits et plus compacts qui sont adressent à des tâches d'application spécifique, plutôt que d'avoir un modèle unique pour l'ensemble de la solution. Dans cet article, je vais vous présenter un concept de création piloté par domaine (DDD) — contexte délimité et vous montrer comment l'appliquer pour construire un modèle ciblé avec EF, en mettant l'accent sur cela avec la grande flexibilité de la fonctionnalité EF Code First. Si vous débutez en DDD, cela une grande approche pour apprendre même si vous n'êtes pas engageant pleinement à DDD. Et si vous utilisez déjà DDD, tu profites en voyant comment vous pouvez utiliser EF tout en suivants des pratiques de DDD.

Conception axée sur le domaine et le contexte délimité

DDD est un sujet assez vaste qui englobe une vue holistique de la conception logicielle. Paul Rayner, qui enseigne des ateliers DDD pour domaine linguistique (DomainLanguage.com), le dit succinctement :

"DDD préconise la conception pragmatique, globale et continue des logiciels : en collaboration avec des experts du domaine pour intégrer des modèles de domaine riche dans le logiciel — modèles qui aident à résoudre des problèmes importants et complexes. »

DDD comprend de nombreux software design patterns, dont — contexte délimité — se prête parfaitement à travailler avec EF. Contexte délimité met l'accent sur le développement de petits modèles qui visent à soutenir des opérations spécifiques dans votre domaine d'activités. Dans son livre, "Domain-Driven Design" (Addison Wesley, 2003), Eric Evans explique que le contexte délimité "délimite l'applicabilité d'un modèle particulier. Contextes de délimitation donne membres de l'équipe une compréhension claire et partagée de ce qui doit être cohérent et ce qui peut se développer indépendamment. »

Les modèles plus petits offrent de nombreux avantages, permettant aux équipes de définir des limites claires relatives aux responsabilités de conception et de développement. Elles conduisent aussi à meilleure facilité de gestion – un contexte ayant une surface plus petite, vous avez moins d'effets secondaires s'inquiéter lors de modifications. En outre, il y a un gain de performances lors de l'EF crée en mémoire des métadonnées d'un modèle lorsqu'il est d'abord chargé en mémoire.

Parce que je construis des contextes délimitées avec EF DbContext, j'ai mentionné mon DbContexts comme « borné DbContexts. » Toutefois, les deux ne sont pas réellement équivalentes : DbContext est une implémentation de la classe alors que le contexte délimité englobe le concept plus large au sein du processus complet de conception. J'appellerai donc mon DBContexts comme « contraints » ou « concentré ».

Comparant une DbContext EF typique à délimité le contexte

Alors que le DDD est plus couramment appliquée au développement d'applications importantes dans les domaines des affaires complexes, apps plus petits peuvent également bénéficier d'un grand nombre de ses leçons. Pour des raisons de cette explication, je vais me concentrer sur une application ciblée à un sous-domaine spécifique : suivi des ventes et du marketing pour une entreprise. Objets impliqués dans la présente demande peuvent aller des clients, des commandes et des éléments de ligne, aux produits, marketing, vendeurs et même les employés. En général un DbContext serait défini pour contenir les propriétés DbSet pour chaque classe dans la solution qui doit être rendue persistante dans la base de données, comme indiqué dans Figure 1.

Figure 1 DbContext typique contenant toutes les Classes de domaine dans la Solution

public class CompanyContext : DbContext
{
  public DbSet<Customer> Customers { get; set; }
  public DbSet<Employee>  Employees { get; set; }
  public DbSet<SalaryHistory> SalaryHistories { get; set; }
  public DbSet<Order> Orders { get; set; }
  public DbSet<LineItem> LineItems { get; set; }
  public DbSet<Product> Products { get; set; }
  public DbSet<Shipment> Shipments { get; set; }
  public DbSet<Shipper> Shippers { get; set; }
  public DbSet<ShippingAddress> ShippingAddresses { get; set; }
  public DbSet<Payment> Payments { get; set; }
  public DbSet<Category> Categories { get; set; }
  public DbSet<Promotion> Promotions { get; set; }
  public DbSet<Return> Returns { get; set; }
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    // Config specifies a 1:0..1 relationship between Customer and ShippingAddress
    modelBuilder.Configurations.Add(new ShippingAddressMap());
  }
}

Imaginez si c'était une application beaucoup plus vaste avec des centaines de classes. Et vous disposez également des configurations de l'API Fluent pour certaines de ces classes. Cela fait pour beaucoup de code pour parcourir et gérer dans une seule catégorie. Avec une grande application, développement pourrait être divisé entre les équipes. Avec cette seule DbContext transverse, chaque équipe aurait besoin un sous-ensemble du code base qui s'étend au-delà de leurs responsabilités. Et modifications de toute l'équipe pour ce contexte pourraient affecter une autre équipe.

Il y a des questions intéressantes que vous pourriez vous poser sur cette vague, Global DbContext. Par exemple, dans le domaine de l'application qui cible le service marketing, utilisateurs ont-ils besoin de travailler avec les données d'historique de la salaire de l'employé ? Le service de livraison doit-il accéder au même niveau de détail sur un client comme un représentant du service clientèle ? Une personne au service de l'expédition devraient modifier un enregistrement de client ? Pour les scénarios les plus courants, la réponse à ces questions serait généralement pas, et ceci pourrait vous aider à voir pourquoi il pouvait judicieux d'avoir plusieurs DbContexts qui gèrent de petits ensembles d'objets de domaine.

Un concentré DbContext pour le service de livraison

Comme DDD recommande de travailler avec des modèles plus petits et plus ciblés avec des limites de contexte bien défini, nous allons réduire la portée de cette DbContext pour cibler les fonctions de service expédition et seulement ces classes nécessaires pour effectuer les tâches pertinentes. Par conséquent, vous pouvez supprimer certaines propriétés DbSet de la DbContext, laissant seulement ceux que vous devrez prendre en charge les fonctionnalités affaires liées à la navigation. J'ai pris les retours, Promotions, catégories, paiements, employés et SalaryHistories :

public class ShippingDeptContext : DbContext
{
  public DbSet<Shipment> Shipments { get; set; }
  public DbSet<Shipper> Shippers { get; set; }
  public DbSet<Customer> Customers { get; set; }
  public DbSet<ShippingAddress> ShippingAddresses { get; set; }
  public DbSet<Order> Order { get; set; }
  public DbSet<LineItem> LineItems { get; set; }
  public DbSet<Product> Products { get; set; }
}

EF Code First utilise les détails de ShippingContext pour déduire le modèle. Figure 2 montre une visualisation du modèle qui sera créée à partir de cette classe, j'ai généré à l'aide de la version bêta de Entity Framework Power Tools 2. Maintenant, nous allons commencer à affiner le modèle.

visualisé modèle depuis le col de la première à la ShippingContext
Figure 2 visualisé modèle depuis le col de la première à la ShippingContext

Tuning le DbContext et en créant des Classes plus ciblées

Il y a encore plus de classes impliquées dans le modèle que j'ai spécifié pour l'expédition. Par convention, Code First inclut toutes les classes qui sont accessibles par d'autres classes dans le modèle. C'est pourquoi catégorie et paiement sont venues même si j'ai enlevé leurs propriétés DbSet. Donc je vais dire le DbContext pour ignorer la catégorie et le paiement :

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Ignore<Category>();
  modelBuilder.Ignore<Payment>();
  modelBuilder.Configurations.Add(new ShippingAddressMap());
}

Cela garantit catégorie et paiement n'obtenir le modèle juste parce qu'ils sont liés au produit et de l'ordre.

Vous pouvez affiner cette classe DbContext encore plus sans affecter le modèle qui en résulte. Avec ces propriétés DbSet, on peut explicitement la requête pour chacune de ces sept ensembles de données dans votre application. Mais si vous pensez à des classes et leur relation, on pourrait croire que, dans ce contexte il ne sera jamais nécessaire de requête pour l'expédition­répondre directement, il peut toujours être trouvé ainsi que les données du client. Même avec aucune ShippingAddresses DbSet, vous pouvez compter sur la même convention qui automatiquement mis dans la catégorie et le paiement d'extraire des ShippingAddress dans le modèle en raison de sa relation au client. Donc, vous pouvez supprimer la propriété de ShippingAddresses sans perdre les mappages de base de données à ShippingAddress. Vous pourriez être en mesure de justifier la suppression d'autres, mais concentrons-nous sur seulement celui-ci :

public class ShippingContext : DbContext
{
  public DbSet<Shipment> Shipments { get; set; }
  public DbSet<Shipper> Shippers { get; set; }
  public DbSet<Customer> Customers { get; set; }
  // Public DbSet<ShippingAddress> ShippingAddresses { get; set; }
  public DbSet<Order> Order { get; set; }
  public DbSet<LineItem> LineItems { get; set; }
  public DbSet<Product> Products { get; set; }
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  { ...
}
}

Dans le cadre du traitement des expéditions, je ne vraiment besoin un objet client complet, un objet de commande complète ou un objet d'élément de commande complet. J'ai ne besoin que le produit à expédier, la quantité (à partir de LineItem), le nom du client et ShippingAddress et toutes les notes qui peuvent être attachées au client ou à l'ordre. Je vais avoir mon DBA créer une vue qui renvoie des éléments non expédiés — ceux avec ShipmentId = 0 ou null. En attendant, je peux définir une classe simplifiée qui sera mappé à ce point de vue avec les propriétés pertinentes que je m'attends à avoir besoin :

[Table("ItemsToBeShipped")]
public class ItemToBeShipped
{
  [Key]
  public int LineItemId { get; set; }
  public int OrderId { get; set; }
  public int ProductId { get; set; }
  public int OrderQty { get; private set; }
  public OrderShippingDetail OrderShippingDetails { get; set; }
}

La logique de traitement des expéditions nécessite l'interrogation pour la ItemToBeShipped et l'obtention de quelque détails de commande, je pourrais avoir besoin avec le client et ShippingAddress. Je pouvais réduire ma définition de DbContext pour Permettez-moi d'interroger un graphique à partir de ce nouveau type en passant la commande, le client et l'expédition à la France­adresse. Cependant, parce que je sais EF pourrait y parvenir avec une requête SQL conçue pour aplatir les résultats et de retourner les commandes répétées, client et la livraison­données d'adresse ainsi que chaque élément de ligne, je vais laisser le programmeur de requête pour une ordonnance et ramener un graphique avec le client et ShippingAddress. Mais encore une fois, je n'ai besoin toutes les colonnes de la table commande, donc je vais créer une classe qui est mieux ciblée pour le service de livraison, y compris les renseignements qui pourraient être imprimées sur un manifeste d'expédition à la France. La classe est la classe de OrderShippingDetail dans Figure 3.

Figure 3 la classe OrderShippingDetail

[Table("Orders")]
public class OrderShippingDetail
{  
  [Key]
  public int OrderId { get; set; }
  public DateTime OrderDate { get; set; }
  public Nullable<DateTime> DueDate { get; set; }
  public string SalesOrderNumber { get; set; }
  public string PurchaseOrderNumber { get; set; }
  public Customer Customer { get; set; }
  public int CustomerId { get; set; }
  public string Comment { get; set; }
  public ICollection<ItemToBeShipped> OpenLineItems { get; set; }
}

Notez que ma classe ItemToBeShipped possède une propriété de navigation pour OrderShippingDetail et OrderShippingDetail a un pour le client. Les propriétés de navigation vont m'aider avec des graphiques lors de l'interrogation et l'enregistrement.

Il y a une pièce de plus à cette énigme. Le service de livraison devront désigner les éléments expédiés et le tableau de commande comporte une colonne de ShipmentId qui est utilisée pour lier un élément de ligne à un envoi. Le $ $ etAPP devez mettre à jour ce champ ShipmentId lorsqu'un article est expédié. Je vais créer une classe simple pour s'occuper de cette tâche, plutôt que de compter sur la classe d'élément de commande qui est utilisée pour les ventes :

[Table("LineItems")]
public class LineItemShipment
{
  [Key]
  public int LineItemId { get; set; }
  public int ShipmentId { get; set; }
}

Chaque fois qu'un élément a été expédié, vous pouvez créer une nouvelle instance de cette classe avec les valeurs correctes et forcer l'application à mettre à jour l'élément de commande dans la base de données. Il sera important de concevoir votre application pour utiliser la classe uniquement à cette fin. Si vous tentez d'insérer un élément de commande en utilisant cette classe au lieu d'un compte pour les champs de la table non nullable comme OrderId, la base de données lève une exception.

Après quelques ajustements plus, mon ShippingContext est maintenant défini comme :

public class ShippingContext : DbContext
{
  public DbSet<Shipment> Shipments { get; set; }
  public DbSet<Shipper> Shippers { get; set; }
  public DbSet<OrderShippingDetail> Order { get; set; }
  public DbSet<ItemToBeShipped> ItemsToBeShipped { get; set; }
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    modelBuilder.Ignore<LineItem>();
    modelBuilder.Ignore<Order>();
    modelBuilder.Configurations.Add(new ShippingAddressMap());
  }
}

Réutiliser la version bêta de Entity Framework Power Tools 2 afin de créer un edmx contenus, je peux voir dans la fenêtre Explorateur de modèles (Figure 4) que le premier Code déduit que le modèle contient les quatre classes spécifiées par le DbSets, ainsi que les clients et les ShippingAddress, qui ont été découverts au moyen de propriétés de navigation de la classe OrderShippingDetail.

modèle parcoureur d'entités ShippingContext tel que déduit par Code d'abord
Figure 4 modèle parcoureur d'entités ShippingContext tel que déduit par Code d'abord

DbContext ciblée et l'initialisation de la base de données

Lorsque vous utilisez une plus petite DbContext qui prend en charge des contextes délimitée dans votre application, il est essentiel de garder à l'esprit deux EF Code First par défaut comportements à l'égard de l'initialisation de la base de données.

La première est que le premier Code recherchera une base de données avec le nom du contexte. Ce n'est pas souhaitable lorsque votre application a un ShippingContext, un CustomerContext, un SalesContext et autres. Au lieu de cela, vous souhaitez que tous les DbContexts pour pointer vers la même base de données.

Le second comportement par défaut à considérer est que Code First utilisera le modèle déduit par un DbContext pour définir le schéma de base de données. Mais maintenant vous avez un DbContext qui représente seulement une tranche de la base de données. Pour cette raison, vous ne voulez pas les classes de DbContext pour déclencher l'initialisation de la base de données.

Il est possible de résoudre ces deux problèmes dans le constructeur de classe de chaque contexte. Par exemple, ici dans la classe de ShippingContext vous pouvez avoir le constructeur spécifier le DPSalesDatabase et désactivez l'initialisation de base de données :

public ShippingContext() : base("DPSalesDatabase")
{
  Database.SetInitializer<ShippingContext>(null);
}

Toutefois, si vous avez beaucoup de DbContext classes dans votre application, cela deviendra un problème de maintenance. Un meilleur modèle consiste à spécifier une classe de base qui désactive l'initialisation de la base de données et définit la base de données en même temps :

public class BaseContext<TContext>
  DbContext where TContext : DbContext
{
  static BaseContext()
  {
    Database.SetInitializer<TContext>(null);
  }
  protected BaseContext() : base("DPSalesDatabase")
  {}
}

Maintenant mes différentes classes de contexte peuvent mettre en œuvre le BaseContext au lieu d'ayant chacune son propre constructeur :

public class ShippingContext:BaseContext<ShippingContext>

Si vous faites un nouveau développement et vous souhaitez laisser le premier Code créer ou migre votre base de données dans vos classes, vous devrez créer un « uber-modèle » à l'aide d'un DbContext qui inclut toutes les classes et les relations nécessaires pour construire un modèle complet qui représente la base de données. Toutefois, ce cadre ne doit pas hériter de BaseContext. Lorsque vous apportez des modifications à vos structures de classe, vous pouvez exécuter du code qui utilise l'uber-contexte pour réaliser l'initialisation de base de données que vous créez ou migration de la base de données.

Exercer le DbContext ciblé

Avec tout cela en place, j'ai créé quelques tests d'intégration automatisée pour effectuer les tâches suivantes :

  • Récupération des éléments de ligne ouvertes.
  • Récupérer la OrderShippingDetails avec le client et la livraison des données pour les commandes d'items non expédiés.
  • Récupérer un élément de ligne non expédié et créer un nouvel envoi. La valeur de l'envoi à la ligne de facturation et insérez la nouvelle livraison dans la base de données en mettant à jour la ligne de facturation dans la base de données avec la valeur de la clé de la nouvelle livraison.

Voici les fonctions les plus critiques, que vous devez effectuer pour l'expédition des produits aux clients qui les ont commandées. Tous les tests passent, vérifier que mon DbContext fonctionne comme prévu. Les tests sont inclus dans le téléchargement d'exemple de cet article.

Pour résumer

J'ai créé non seulement un DbContext qui vise spécifiquement à soutenir les tâches de navigation, mais il pense à travers aussi m'a aidé à créer des objets de domaine plus efficace à utiliser avec ces tâches. L'utilisation de DbContext pour aligner mon contexte délimité avec mon expédition sous-domaine de cette façon signifie que je n'ai pas à patauger dans un bourbier de code pour utiliser la fonctionnalité de service expédition de la demande, et je peux faire ce que je dois les objets domaine spécialisé sans affecter les autres zones de l'effort de développement.

Vous pouvez voir d'autres exemples de mise au point de DbContext en utilisant le concept DDD du contexte délimité dans le livre, "programmation Entity Framework : DbContext"(o ' Reilly Media, 2011), dont j'ai coécrit avec Rowan Miller.

Julie Lerman est une Microsoft MVP, mentor et conseillère .NET qui habite dans les collines du Vermont. Elle participe régulièrement à des groupes d’utilisateurs et à des conférences dans le monde entier, où elle partage son savoir-faire dans le domaine de l’accès aux données et d’autres sujets liés à Microsoft .NET. Son blog est accessible à l'adresse thedatafarm.com/blog. Elle est en outre l'auteur de « Programming Entity Framework » (2010), ainsi que d'une édition Code First (2011) et d'une édition DbContext (2012), publiées chez O’Reilly Media. Vous pouvez la suivre sur Twitter à l'adresse twitter.com/julielerman.

Merci à l'expert technique suivant d'avoir relu cet article : Paul Rayner