Implementación de la herencia con Entity Framework en una aplicación MVC de ASP.NET (8 de 10)
por Tom Dykstra
En la aplicación web de ejemplo Contoso University, se muestra cómo crear aplicaciones ASP.NET MVC 4 con Code First de Entity Framework 5 y Visual Studio 2012. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial de la serie.
Nota:
Si se encontrase problemas que no pudiera resolver, descargue el capítulo completo e intente reproducir el problema. Por lo general, es posible encontrar la solución al problema comparando el propio código con el código completado. Para conocer algunos errores comunes y cómo resolverlos, consulte Errores y soluciones alternativas.
En el tutorial anterior, se trataron las excepciones de simultaneidad. En este tutorial se muestra cómo implementar la herencia en el modelo de datos.
En la programación orientada a objetos, puede usar la herencia para eliminar el código redundante. En este tutorial, cambiará las clases Instructor
y Student
para que deriven de una clase base Person
que contenga propiedades como LastName
, que son comunes tanto para los instructores como para los alumnos. No tendrá que agregar ni cambiar ninguna página web, sino que cambiará parte del código y esos cambios se reflejarán automáticamente en la base de datos.
Herencia de tabla por jerarquía frente a herencia de tabla por tipo
En la programación orientada a objetos, puede usar la herencia para facilitar el trabajo con clases relacionadas. Por ejemplo, las clases Instructor
y Student
del modelo de datos School
comparten varias propiedades, lo que da lugar a código redundante:
Imagine que quiere eliminar el código redundante de las propiedades que comparten las entidades Instructor
y Student
. Puede crear una clase base Person
que solo contenga las propiedades compartidas y después hacer que las entidades Instructor
y Student
hereden de esa clase base, como se muestra en la siguiente ilustración:
Esta estructura de herencia se puede representar de varias formas en la base de datos. Puede tener una sola tabla Person
que incluya información sobre los alumnos y los instructores. Algunas de las columnas solo podrían aplicarse a los instructores (HireDate
), algunas solo a los alumnos (EnrollmentDate
) y algunas a ambas (LastName
, FirstName
). Lo más común sería que tuviera una columna discriminadora para indicar qué tipo representa cada fila. Por ejemplo, en la columna discriminadora podría aparecer "Instructor" para los instructores y "Student" para los alumnos.
Este patrón de generación de una estructura de herencia de la entidad a partir de una tabla de base de datos única, se denomina herencia de tabla por jerarquía (TPH) .
Una alternativa consiste en hacer que la base de datos se parezca más a la estructura de herencia. Por ejemplo, podría tener solo los campos de nombre en la tabla Person
y tablas Instructor
y Student
independientes con los campos de fecha.
Este patrón de creación de una tabla de base de datos para cada clase de entidad se denomina herencia de tabla por tipo (TPT).
Los patrones de herencia TPH suelen ofrecer un mejor rendimiento en Entity Framework que los patrones de herencia TPT, porque los patrones TPT pueden dar lugar a consultas join complejas. Este tutorial muestra cómo implementar la herencia de TPH. Para ello, realice los pasos siguientes:
- Cree una clase
Person
y cambie las clasesInstructor
yStudent
para derivar dePerson
. - Agregue código de asignación de modelo a base de datos a la clase de contexto de base de datos.
- Cambie las referencias
InstructorID
yStudentID
en todo el proyecto porPersonID
.
Creación de la clase Person
Nota: No podrá compilar el proyecto después de crear las clases siguientes hasta que actualice los controladores que usan estas clases.
En la carpeta Models, cree Person.cs y reemplace el código de plantilla por el código siguiente:
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;
}
}
}
}
En Instructor.cs, derive la clase Instructor
de la clase Person
y quite los campos de clave y nombre. El código tendrá un aspecto similar al ejemplo siguiente:
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; }
}
}
Realice cambios similares a Student.cs. La clase Student
tendrá un aspecto similar al ejemplo siguiente:
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; }
}
}
Adición del tipo de entidad Person al modelo
En SchoolContext.cs, agregue una propiedad DbSet
para el tipo de entidad Person
:
public DbSet<Person> People { get; set; }
Esto es todo lo que Entity Framework necesita para configurar la herencia de tabla por jerarquía. Como verá, cuando se vuelva a crear la base de datos, tendrá una tabla Person
en lugar de las tablas Student
y Instructor
.
Cambio de InstructorID y StudentID a PersonID
En SchoolContext.cs, en la instrucción asignación Instructor-Course, cambie MapRightKey("InstructorID")
a MapRightKey("PersonID")
:
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("PersonID")
.ToTable("CourseInstructor"));
Este cambio no es necesario; simplemente cambia el nombre de la columna InstructorID en la tabla de combinación de varios a varios. Si deja el nombre como InstructorID, la aplicación seguirá funcionando correctamente. Este es el SchoolContext.cs completado:
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"));
}
}
}
A continuación, debe cambiar InstructorID
por PersonID
y StudentID
por PersonID
en todo el proyecto excepto en los archivos de migraciones con marca de tiempo en la carpeta Migraciones. Para ello, encontrará y abrirá solo los archivos que deben cambiarse y, a continuación, realice un cambio global en los archivos abiertos. El único archivo de la carpeta Migraciones que debe cambiar es MigrationsConfiguration.cs..
-
Importante
Comience cerrando todos los archivos abiertos en Visual Studio.
Haga clic en Buscar y reemplazar -- Buscar todos los archivos en el menú Editar y, a continuación, busque todos los archivos del proyecto que contengan
InstructorID
.Abra cada archivo de la ventana Buscar resultadosexcepto los archivos de migración <time-stamp>_.cs de la carpeta Migraciones, haciendo doble clic en una línea de cada archivo.
Abra el cuadro de diálogo Reemplazar en archivos y cambie Mirar en a Todos los documentos abiertos.
Utilice el cuadro de diálogo Reemplazar en archivos para cambiar todos los
InstructorID
porPersonID.
.Encuentre todos los archivos del proyecto que contengan
StudentID
.Abra cada archivo de la ventana Buscar resultadosexcepto los archivos de migración <time-stamp>_*.cs de la carpeta Migraciones, haciendo doble clic en una línea de cada archivo.
Abra el cuadro de diálogo Reemplazar en archivos y cambie Mirar en a Todos los documentos abiertos.
Utilice el cuadro de diálogo Reemplazar en archivos para cambiar todos los
StudentID
porPersonID
.Compile el proyecto.
(Observe que esto demuestra una desventaja del patrón classnameID
para nombrar claves principales. Si hubiera asignado el identificador de claves principales sin tener que prefijar el nombre de clase, ahora no sería necesario cambiar el nombre.
Crear y actualizar un archivo de migraciones
En la Consola del administrador de paquetes (PMC), escriba el comando siguiente:
Add-Migration Inheritance
Ejecute el comando Update-Database
de PMC. El comando producirá un error en este punto porque tenemos datos existentes que las migraciones no saben cómo controlar. Verá este error:
La instrucción ALTER TABLE entra en conflicto con la restricción FOREIGN KEY "FK_dbo.Department_dbo.Person_PersonID". El conflicto se produjo en la base de datos "ContosoUniversity", tabla "dbo. Person", columna 'PersonID'.
Abra Migrations<timestamp>_Inheritance.cs y reemplace el método Up
por el código siguiente:
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");
}
Vuelva a ejecutar el comando update-database
.
Nota:
Al migrar datos y hacer cambios en el esquema, es posible que se generen otros errores. Si recibe errores de migración que no puede resolver, puede continuar con el tutorial cambiando la cadena de conexión en el archivo Web.config o eliminando la base de datos. El enfoque más sencillo consiste en cambiar el nombre de la base de datos en el archivo Web.config. Por ejemplo, cambie el nombre de la base de datos a CU_test como se muestra en el ejemplo siguiente:
<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" />
Con una base de datos nueva, no hay ningún dato para migrar y es mucho más probable que el comando update-database
se complete sin errores. Para obtener instrucciones sobre cómo eliminar la base de datos, vea Procedimiento para quitar una base de datos de Visual Studio 2012. Si adopta este enfoque para continuar con el tutorial, omita el paso de implementación al final de este tutorial, ya que el sitio implementado obtendría el mismo error al ejecutar las migraciones automáticamente. Si desea solucionar un error de migración, el mejor recurso es uno de los foros de Entity Framework o StackOverflow.com.
Prueba
Ejecute el sitio y pruebe varias páginas. Todo funciona igual que antes.
En Explorador de servidores, expanda SchoolContext y después Tables, y verá que las tablas Student e Instructor se han reemplazado por una tabla Person. Abra el diseñador de la tabla Person y verá que contiene todas las columnas que solía haber en las tablas Student e Instructor.
Haga clic con el botón derecho en la tabla Person y después haga clic en Mostrar datos de tabla para ver la columna discriminadora.
En el diagrama siguiente, se muestra la estructura de la nueva base de datos School:
Resumen
La herencia de tabla por jerarquía se ha implementado ahora para las clases Person
, Student
y Instructor
. Para obtener más información sobre esta y otras estructuras de herencia, vea Estrategias de asignación de herencia en el blog de Morteza Manavi. En el siguiente tutorial verá algunas maneras de implementar el repositorio y los patrones de unidad de trabajo.
En el mapa de contenido de acceso a datos de ASP.NET se pueden encontrar vínculos a otros recursos de Entity Framework.