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 :
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 :
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.
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.
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 lesInstructor
classes etStudent
de manière à dériver dePerson
. - 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érencezStudentID
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.
-
Important
Commencez par fermer tous les fichiers ouverts dans Visual Studio.
Cliquez sur Rechercher et remplacer : recherchez tous les fichiers dans le menu Modifier , puis recherchez tous les fichiers du projet qui contiennent
InstructorID
.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.
Ouvrez la boîte de dialogue Remplacer dans les fichiers et remplacez Look in parTous les documents ouverts.
Utilisez la boîte de dialogue Remplacer dans les fichiers pour tout modifier
InstructorID
enPersonID.
Recherchez tous les fichiers du projet qui contiennent
StudentID
.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.
Ouvrez la boîte de dialogue Remplacer dans les fichiers et remplacez Look in parTous les documents ouverts.
Utilisez la boîte de dialogue Remplacer dans les fichiers pour tout remplacer
StudentID
parPersonID
.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 .
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.
Le diagramme suivant illustre la structure de la nouvelle base de données School :
Résumé
L’héritage table par hiérarchie a maintenant été implémenté pour les Person
classes , Student
et 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.