Compartir vía


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:

Screenshots that show the Student and Instructor classes with redundant codes highlighted.

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:

Screenshot that shows the Student and Instructor classes deriving from the Person class.

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.

Screenshot that shows the inheritance structure from the Person entity class.

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.

Screenshot that shows new Instructor and Student database tables deriving from the Person entity class.

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 clases Instructor y Student para derivar de Person.
  • 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 y StudentID en todo el proyecto por PersonID.

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..

  1. Importante

    Comience cerrando todos los archivos abiertos en Visual Studio.

  2. 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.

    Screenshot that shows the Find and Replace window. Instructor I D, Current Project, Match case and Match whole word checkboxes, and Find All button are all highlighted.

  3. 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.

    Screenshot that shows Find Results window. The time stamp migration files are crossed out in red.

  4. Abra el cuadro de diálogo Reemplazar en archivos y cambie Mirar en a Todos los documentos abiertos.

  5. Utilice el cuadro de diálogo Reemplazar en archivos para cambiar todos los InstructorID por PersonID..

    Screenshot that shows the Find and Replace window. Person I D is entered in the Replace with text field.

  6. Encuentre todos los archivos del proyecto que contengan StudentID.

  7. 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.

    Screenshot that shows the Find Results window. The time stamp migration files are crossed out.

  8. Abra el cuadro de diálogo Reemplazar en archivos y cambie Mirar en a Todos los documentos abiertos.

  9. Utilice el cuadro de diálogo Reemplazar en archivos para cambiar todos los StudentID por PersonID.

    Screenshot that shows the Find and Replace window. Replace in Files, All Open Documents, Match case and Match whole word checkboxes, and Replace All button are highlighted.

  10. 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.

Screenshot that shows the Server Explorer window. The Data Connections, School Context, and Tables tabs are expanded to show the Person table.

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.

Screenshot that shows the Person table. The Discriminator column name is highlighted.

En el diagrama siguiente, se muestra la estructura de la nueva base de datos School:

Screenshot that shows the School database diagram.

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.