Partager via


Implémentation de l’héritage avec Entity Framework dans une application MVC ASP.NET (8 sur 10)

par Tom Dykstra

L’exemple d’application web Contoso University montre comment créer ASP.NET applications MVC 4 à l’aide d’Entity Framework 5 Code First et de Visual Studio 2012. Pour obtenir des informations sur la série de didacticiels, consultez le premier didacticiel de la série.

Notes

Si vous rencontrez un problème que vous ne pouvez pas résoudre, téléchargez le chapitre terminé et essayez de reproduire votre problème. Vous pouvez généralement trouver la solution au problème en comparant votre code au code terminé. Pour connaître certaines erreurs courantes et savoir comment les résoudre, consultez Erreurs et solutions de contournement.

Dans le tutoriel précédent, vous avez géré les exceptions d’accès concurrentiel. Ce didacticiel vous indiquera comment implémenter l’héritage dans le modèle de données.

Dans la programmation orientée objet, vous pouvez utiliser l’héritage pour éliminer le code redondant. Dans ce didacticiel, vous allez modifier les classes Instructor et Student afin qu’elles dérivent d’une classe de base Person qui contient des propriétés telles que LastName, communes aux formateurs et aux étudiants. Vous n’ajouterez ni ne modifierez aucune page web, mais vous modifierez une partie du code et ces modifications seront automatiquement répercutées dans la base de données.

Héritage table par hiérarchie et table par type

Dans la programmation orientée objet, vous pouvez utiliser l’héritage pour faciliter l’utilisation des classes associées. Par exemple, les Instructor classes et Student dans le School modèle de données partagent plusieurs propriétés, ce qui entraîne un code redondant :

Captures d’écran montrant les classes Student et Instructor avec des codes redondants mis en surbrillance.

Supposons que vous souhaitez éliminer le code redondant pour les propriétés partagées par les entités Instructor et Student. Vous pouvez créer une Person classe de base qui contient uniquement ces propriétés partagées, puis faire en sorte que les Instructor entités et Student héritent de cette classe de base, comme illustré dans l’illustration suivante :

Capture d’écran montrant les classes Student et Instructor dérivant de la classe Person.

Il existe plusieurs façons de représenter cette structure d’héritage dans la base de données. Vous pouvez avoir une table Person qui inclut des informations sur les étudiants et les formateurs dans une table unique. Certaines colonnes peuvent s’appliquer uniquement aux instructeurs (HireDate), d’autres uniquement aux étudiants (EnrollmentDate), d’autres aux deux (LastName, FirstName). En règle générale, vous disposez d’une colonne de discriminateur pour indiquer le type que chaque ligne représente. Par exemple, la colonne de discriminateur peut avoir « Instructor » pour les formateurs et « Student » pour les étudiants.

Capture d’écran montrant la structure d’héritage de la classe d’entité Person.

Ce modèle de génération d’une structure d’héritage d’entité à partir d’une table de base de données unique est appelé héritage table par hiérarchie (TPH).

Une alternative consiste à faire en sorte que la base de données ressemble plus à la structure d’héritage. Par exemple, vous pouvez avoir uniquement les champs de nom dans la table Person, et des tables Instructor et Student distinctes avec les champs de date.

Capture d’écran montrant les nouvelles tables de base de données Instructor et Student dérivant de la classe d’entité Person.

Ce modèle de création d’une table de base de données pour chaque classe d’entité est appelé héritage table par type (TPT).

Les modèles d’héritage TPH offrent généralement de meilleures performances dans Entity Framework que les modèles d’héritage TPT, car les modèles TPT peuvent entraîner des requêtes de jointure complexes. Ce didacticiel montre comment implémenter l’héritage TPH. Pour ce faire, procédez comme suit :

  • Créez une Person classe et modifiez les Instructor classes et Student de manière à dériver de Person.
  • Ajoutez du code de mappage de modèle à base de données à la classe de contexte de base de données.
  • Modifiez InstructorID et référencez StudentID dans l’ensemble du projet à PersonID.

Création de la classe Person

Remarque : Vous ne pourrez pas compiler le projet après avoir créé les classes ci-dessous tant que vous n’aurez pas mis à jour les contrôleurs qui utilisent ces classes.

Dans le dossier Models , créez Person.cs et remplacez le code du modèle par le code suivant :

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
   public abstract class Person
   {
      [Key]
      public int PersonID { get; set; }

      [RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
      [StringLength(50, MinimumLength = 1)]
      [Display(Name = "Last Name")]
      public string LastName { get; set; }

      [Column("FirstName")]
      [Display(Name = "First Name")]
      [StringLength(50, MinimumLength = 2, ErrorMessage = "First name must be between 2 and 50 characters.")]
      public string FirstMidName { get; set; }

      public string FullName
      {
         get
         {
            return LastName + ", " + FirstMidName;
         }
      }
   }
}

Dans Instructor.cs, dérivez la Instructor classe de la Person classe et supprimez les champs clé et nom. Le code ressemblera à l’exemple suivant :

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Instructor : Person
    {
        [DataType(DataType.Date)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        public virtual ICollection<Course> Courses { get; set; }
        public virtual OfficeAssignment OfficeAssignment { get; set; }
    }
}

Apportez des modifications similaires à Student.cs. La Student classe ressemblera à l’exemple suivant :

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [DataType(DataType.Date)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }

        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

Ajout du type d’entité Person au modèle

Dans SchoolContext.cs, ajoutez une DbSet propriété pour le type d’entité Person :

public DbSet<Person> People { get; set; }

C’est là tout ce dont Entity Framework a besoin pour configurer l’héritage TPH (table par hiérarchie). Comme vous le verrez, lorsque la base de données est recréé, une table est Person à la Student place des tables et Instructor .

Modification de InstructorID et StudentID en PersonID

Dans SchoolContext.cs, dans l’instruction de mappage Instructor-Course, remplacez par MapRightKey("InstructorID")MapRightKey("PersonID"):

modelBuilder.Entity<Course>()
    .HasMany(c => c.Instructors).WithMany(i => i.Courses)
    .Map(t => t.MapLeftKey("CourseID")
    .MapRightKey("PersonID")
    .ToTable("CourseInstructor"));

Cette modification n’est pas requise ; il modifie simplement le nom de la colonne InstructorID dans la table de jointure plusieurs-à-plusieurs. Si vous avez laissé le nom InstructorID, l’application fonctionne toujours correctement. Voici le SchoolContext.cs terminé :

using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace ContosoUniversity.DAL
{
   public class SchoolContext : DbContext
   {
      public DbSet<Course> Courses { get; set; }
      public DbSet<Department> Departments { get; set; }
      public DbSet<Enrollment> Enrollments { get; set; }
      public DbSet<Instructor> Instructors { get; set; }
      public DbSet<Student> Students { get; set; }
      public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
      public DbSet<Person> People { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
         modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

         modelBuilder.Entity<Course>()
             .HasMany(c => c.Instructors).WithMany(i => i.Courses)
             .Map(t => t.MapLeftKey("CourseID")
                 .MapRightKey("PersonID")
                 .ToTable("CourseInstructor"));
      }
   }
}

Ensuite, vous devez passer InstructorID à PersonID et StudentID à PersonID tout le projet , sauf dans les fichiers de migration horodatés dans le dossier Migrations . Pour ce faire, vous trouverez et ouvrez uniquement les fichiers qui doivent être modifiés, puis effectuez une modification globale sur les fichiers ouverts. Le seul fichier dans le dossier Migrations que vous devez modifier est Migrations\Configuration.cs.

  1. Important

    Commencez par fermer tous les fichiers ouverts dans Visual Studio.

  2. Cliquez sur Rechercher et remplacer : recherchez tous les fichiers dans le menu Modifier , puis recherchez tous les fichiers du projet qui contiennent InstructorID.

    Capture d’écran montrant la fenêtre Rechercher et remplacer. Les cases à cocher I D de l’instructeur, Projet actuel, Correspondance avec la casse et Mettre en correspondance le mot entier, et le bouton Rechercher tout sont tous mis en surbrillance.

  3. Ouvrez chaque fichier dans la fenêtre Résultats de la recherche, à l’exception des <fichiers de migration time-stamp>_.cs dans le dossier Migrations , en double-cliquant sur une ligne pour chaque fichier.

    Capture d’écran montrant la fenêtre Résultats de la recherche. Les fichiers de migration d’horodatage sont barrés en rouge.

  4. Ouvrez la boîte de dialogue Remplacer dans les fichiers et remplacez Look in parTous les documents ouverts.

  5. Utilisez la boîte de dialogue Remplacer dans les fichiers pour tout modifier InstructorID en PersonID.

    Capture d’écran montrant la fenêtre Rechercher et remplacer. L’ID de personne est entré dans le champ Remplacer par du texte.

  6. Recherchez tous les fichiers du projet qui contiennent StudentID.

  7. Ouvrez chaque fichier dans la fenêtre Résultats de la recherche, à l’exception des <fichiers de migration time-stamp>_*.cs dans le dossier Migrations , en double-cliquant sur une ligne pour chaque fichier.

    Capture d’écran montrant la fenêtre Résultats de la recherche. Les fichiers de migration d’horodatage sont barrés.

  8. Ouvrez la boîte de dialogue Remplacer dans les fichiers et remplacez Look in parTous les documents ouverts.

  9. Utilisez la boîte de dialogue Remplacer dans les fichiers pour tout remplacer StudentID par PersonID.

    Capture d’écran montrant la fenêtre Rechercher et remplacer. Les cases à cocher Remplacer dans fichiers, Tous les documents ouverts, Mettre en correspondance la casse et Mettre en correspondance les mots entiers et le bouton Remplacer tout sont mis en surbrillance.

  10. Créez le projet.

(Notez que cela illustre un inconvénient du classnameID modèle de nommage des clés primaires. Si vous aviez nommé l’ID des clés primaires sans préfixer le nom de la classe, aucun changement de nom n’est nécessaire pour l’instant.)

Créer et mettre à jour un fichier de migrations

Dans la console du Gestionnaire de package (PMC), entrez la commande suivante :

Add-Migration Inheritance

Exécutez la Update-Database commande dans le PMC. La commande échoue à ce stade, car nous avons des données existantes que les migrations ne savent pas comment gérer. Vous obtenez l’erreur suivante :

L’instruction ALTER TABLE a été en conflit avec la contrainte FOREIGN KEY « FK_dbo. Department_dbo. Person_PersonID ». Le conflit s’est produit dans la base de données « ContosoUniversity », table « dbo ». Person », colonne 'PersonID'.

Ouvrez Migrations< ; timestamp>_Inheritance.cs et remplacez la Up méthode par le code suivant :

public override void Up()
{
    DropForeignKey("dbo.Department", "InstructorID", "dbo.Instructor");
    DropForeignKey("dbo.OfficeAssignment", "InstructorID", "dbo.Instructor");
    DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student");
    DropForeignKey("dbo.CourseInstructor", "InstructorID", "dbo.Instructor");
    DropIndex("dbo.Department", new[] { "InstructorID" });
    DropIndex("dbo.OfficeAssignment", new[] { "InstructorID" });
    DropIndex("dbo.Enrollment", new[] { "StudentID" });
    DropIndex("dbo.CourseInstructor", new[] { "InstructorID" });
    RenameColumn(table: "dbo.Department", name: "InstructorID", newName: "PersonID");
    RenameColumn(table: "dbo.OfficeAssignment", name: "InstructorID", newName: "PersonID");
    RenameColumn(table: "dbo.Enrollment", name: "StudentID", newName: "PersonID");
    RenameColumn(table: "dbo.CourseInstructor", name: "InstructorID", newName: "PersonID");
    CreateTable(
        "dbo.Person",
        c => new
            {
                PersonID = c.Int(nullable: false, identity: true),
                LastName = c.String(maxLength: 50),
                FirstName = c.String(maxLength: 50),
                HireDate = c.DateTime(),
                EnrollmentDate = c.DateTime(),
                Discriminator = c.String(nullable: false, maxLength: 128),
                OldId = c.Int(nullable: false)
            })
        .PrimaryKey(t => t.PersonID);

    // Copy existing Student and Instructor data into new Person table.
    Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, StudentId AS OldId FROM dbo.Student");
    Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, HireDate, null AS EnrollmentDate, 'Instructor' AS Discriminator, InstructorId AS OldId FROM dbo.Instructor");

    // Fix up existing relationships to match new PK's.
    Sql("UPDATE dbo.Enrollment SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = Enrollment.PersonId AND Discriminator = 'Student')");
    Sql("UPDATE dbo.Department SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = Department.PersonId AND Discriminator = 'Instructor')");
    Sql("UPDATE dbo.OfficeAssignment SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = OfficeAssignment.PersonId AND Discriminator = 'Instructor')");
    Sql("UPDATE dbo.CourseInstructor SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = CourseInstructor.PersonId AND Discriminator = 'Instructor')");

    // Remove temporary key
    DropColumn("dbo.Person", "OldId");

    AddForeignKey("dbo.Department", "PersonID", "dbo.Person", "PersonID");
    AddForeignKey("dbo.OfficeAssignment", "PersonID", "dbo.Person", "PersonID");
    AddForeignKey("dbo.Enrollment", "PersonID", "dbo.Person", "PersonID", cascadeDelete: true);
    AddForeignKey("dbo.CourseInstructor", "PersonID", "dbo.Person", "PersonID", cascadeDelete: true);
    CreateIndex("dbo.Department", "PersonID");
    CreateIndex("dbo.OfficeAssignment", "PersonID");
    CreateIndex("dbo.Enrollment", "PersonID");
    CreateIndex("dbo.CourseInstructor", "PersonID");
    DropTable("dbo.Instructor");
    DropTable("dbo.Student");
}

Exécutez de nouveau la commande update-database.

Notes

Il est possible d’obtenir d’autres erreurs lors de la migration des données et de l’apport de modifications de schéma. Si vous obtenez des erreurs de migration que vous ne pouvez pas résoudre, vous pouvez poursuivre le didacticiel en modifiant le chaîne de connexion dans le fichier Web.config ou en supprimant la base de données. L’approche la plus simple consiste à renommer la base de données dans le fichier Web.config . Par exemple, remplacez le nom de la base de données par CU_test comme indiqué dans l’exemple suivant :

<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;
      Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\CU_Test.mdf" 
      providerName="System.Data.SqlClient" />

Avec une nouvelle base de données, il n’y a pas de données à migrer, et la update-database commande est beaucoup plus susceptible de se terminer sans erreurs. Pour obtenir des instructions sur la suppression de la base de données, consultez Comment supprimer une base de données de Visual Studio 2012. Si vous suivez cette approche pour poursuivre le didacticiel, ignorez l’étape de déploiement à la fin de ce didacticiel, car le site déployé obtiendrait la même erreur lorsqu’il exécute automatiquement des migrations. Si vous souhaitez résoudre une erreur de migration, la meilleure ressource est l’un des forums ou StackOverflow.com Entity Framework.

Test

Exécutez le site et essayez différentes pages. Tout fonctionne comme avant.

Dans Server Explorer, développez SchoolContext, puis Tables, et vous voyez que les tables Student et Instructor ont été remplacées par une table Person. Développez la table Person et vous voyez qu’elle contient toutes les colonnes qui se trouveraient auparavant dans les tables Student et Instructor .

Capture d’écran montrant la fenêtre Server Explorer. Les onglets Connexions de données, Contexte scolaire et Tables sont développés pour afficher la table Person.

Cliquez avec le bouton droit sur la table Person, puis cliquez sur Afficher les données de la table pour voir la colonne de discriminateur.

Capture d’écran montrant la table Person. Le nom de la colonne discriminateur est mis en surbrillance.

Le diagramme suivant illustre la structure de la nouvelle base de données School :

Capture d’écran montrant le diagramme de base de données School.

Résumé

L’héritage table par hiérarchie a maintenant été implémenté pour les Personclasses , Studentet Instructor . Pour plus d’informations sur cette structure d’héritage et sur d’autres structures d’héritage, consultez stratégies de mappage d’héritage sur le blog de Morteza Manavi. Dans le tutoriel suivant, vous verrez quelques façons d’implémenter le référentiel et l’unité de travail modèles.

Vous trouverez des liens vers d’autres ressources Entity Framework dans le ASP.NET Data Access Content Map.