Tutorial: Implementación de la herencia: ASP.NET MVC con EF Core

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 facilitar la reutilización del código. 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.

En este tutorial ha:

  • Asigna la herencia a la base de datos
  • Creación de la clase Person
  • Actualiza Instructor y Student
  • Agrega Person al modelo
  • Crea y actualiza migraciones
  • Prueba la implementación

Requisitos previos

Asigna la herencia a la base de datos

Las clases Instructor y Student del modelo de datos School tienen varias propiedades idénticas:

Student and Instructor classes

Imagine que quiere eliminar el código redundante de las propiedades que comparten las entidades Instructor y Student. O que quiere escribir un servicio que pueda dar formato a los nombres sin preocuparse de si el nombre es de un instructor o de un alumno. Puede crear una clase base Person que solo contenga las propiedades compartidas y después hacer que las clases Instructor y Student hereden de esa clase base, como se muestra en la siguiente ilustración:

Student and Instructor classes deriving from 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 podrían aplicarse solo a los instructores (HireDate), otras solo a los alumnos (EnrollmentDate) y otras a ambos (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.

Table-per-hierarchy example

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.

Advertencia

La tabla por tipo (TPT) no es compatible con EF Core 3.x, pero se ha implementado en EF Core 5.0.

Table-per-type inheritance

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

Y todavía otra opción pasa por asignar todos los tipos no abstractos a tablas individuales. Todas las propiedades de una clase, incluidas las propiedades heredadas, se asignan a columnas de la tabla correspondiente. Este patrón se denomina herencia de tabla por clase concreta (TPC) . Si implementara la herencia de TPC en las clases Person, Student e Instructor tal como se ha mostrado antes, las tablas Student e Instructor se verían igual antes que después de implementar la herencia.

Los patrones de herencia de TPC y TPH acostumbran a tener un mejor rendimiento que los patrones de herencia de TPT, porque estos pueden provocar consultas de combinaciones complejas.

Este tutorial muestra cómo implementar la herencia de TPH. TPH es el único patrón de herencia que Entity Framework Core admite. Lo que tiene que hacer es crear una clase Person, cambiar las clases Instructor y Student que deriven de Person, agregar la nueva clase a DbContext y crear una migración.

Sugerencia

Considere la posibilidad de guardar una copia del proyecto antes de realizar los siguientes cambios. De este modo, si tiene problemas y necesita volver a empezar, le resultará más fácil comenzar desde el proyecto guardado que tener que revertir los pasos realizados en este tutorial o volver al principio de toda la serie.

Creación de la clase Person

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
    {
        public int ID { get; set; }

        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }
    }
}

Actualiza Instructor y Student

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;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

Realice los mismos cambios en Student.cs.

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

namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }


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

Agrega Person al modelo

Agregue el tipo de entidad Person a SchoolContext.cs. Se resaltan las líneas nuevas.

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }
        public DbSet<Person> People { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
            modelBuilder.Entity<Person>().ToTable("Person");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

Esto es todo lo que Entity Framework necesita para configurar la herencia de tabla por jerarquía. Como verá, cuando la base de datos esté actualizada, tendrá una tabla Person en lugar de las tablas Student e Instructor.

Crea y actualiza migraciones

Guarde los cambios y compile el proyecto. A continuación, abra la ventana de comandos en la carpeta de proyecto y escriba el siguiente comando:

dotnet ef migrations add Inheritance

No ejecute el comando database update todavía. Este comando provocará la pérdida de datos porque colocará la tabla Instructor y cambiará el nombre de la tabla Student por Person. Deberá proporcionar código personalizado para conservar los datos existentes.

Abra Migrations/<timestamp>_Inheritance.cs y reemplace el método Up por el código siguiente:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropForeignKey(
        name: "FK_Enrollment_Student_StudentID",
        table: "Enrollment");

    migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table: "Enrollment");

    migrationBuilder.RenameTable(name: "Instructor", newName: "Person");
    migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<string>(name: "Discriminator", table: "Person", nullable: false, maxLength: 128, defaultValue: "Instructor");
    migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<int>(name: "OldId", table: "Person", nullable: true);

    // Copy existing Student data into new Person table.
    migrationBuilder.Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, ID AS OldId FROM dbo.Student");
    // Fix up existing relationships to match new PK's.
    migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator = 'Student')");

    // Remove temporary key
    migrationBuilder.DropColumn(name: "OldID", table: "Person");

    migrationBuilder.DropTable(
        name: "Student");

    migrationBuilder.CreateIndex(
         name: "IX_Enrollment_StudentID",
         table: "Enrollment",
         column: "StudentID");

    migrationBuilder.AddForeignKey(
        name: "FK_Enrollment_Person_StudentID",
        table: "Enrollment",
        column: "StudentID",
        principalTable: "Person",
        principalColumn: "ID",
        onDelete: ReferentialAction.Cascade);
}

Este código se encarga de las siguientes tareas de actualización de la base de datos:

  • Quita las restricciones de la clave externa y los índices que apuntan a la tabla Student.

  • Cambia el nombre de la tabla Instructor por Person y realiza los cambios necesarios para que pueda almacenar datos de los alumnos:

  • Agrega EnrollmentDate que acepta valores NULL para alumnos.

  • Agrega la columna discriminadora para indicar si una fila es para un alumno o para un instructor.

  • Hace que HireDate admita un valor NULL, puesto que las filas de alumnos no dispondrán de fechas de contratación.

  • Agrega un campo temporal que se usará para actualizar las claves externas que apuntan a los alumnos. Cuando copie alumnos en la tabla Person, obtendrán nuevos valores de clave principal.

  • Copia datos de la tabla Student a la tabla Person. Esto hace que los alumnos se asignen a nuevos valores de clave principal.

  • Corrige los valores de clave externa correspondientes a los alumnos.

  • Vuelve a crear las restricciones y los índices de la clave externa, pero ahora los dirige a la tabla Person.

(Si hubiera usado el GUID en lugar de un número entero como tipo de clave principal, los valores de la clave principal de alumno no tendrían que cambiar y algunos de estos pasos se podrían haber omitido).

Ejecute el comando database update:

dotnet ef database update

(En un sistema de producción haría los cambios correspondientes en el método Down por si alguna vez tuviera que usarlo para volver a la versión anterior de la base de datos. Para este tutorial, no usará el método Down).

Nota:

Al hacer cambios en el esquema, se pueden generar otros errores en una base de datos que contenga los datos existentes. Si se producen errores de migración que no se pueden resolver, puede cambiar el nombre de la base de datos en la cadena de conexión o eliminar la base de datos. Con una nueva base de datos, no habrá ningún dato para migrar y es más probable que el comando de actualización de base de datos se complete sin errores. Para eliminar la base de datos, use SSOX o ejecute el comando database drop de la CLI.

Prueba la implementación

Ejecute la aplicación y haga la prueba en distintas páginas. Todo funciona igual que antes.

En Explorador de objetos de SQL Server, expanda Conexiones de datos/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.

Person table in SSOX

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.

Person table in SSOX - table data

Obtención del código

Descargue o vea la aplicación completa.

Recursos adicionales

Para obtener más información sobre la herencia en Entity Framework Core, consulte Herencia.

Pasos siguientes

En este tutorial ha:

  • Asignado la herencia a la base de datos
  • Creado la clase Person
  • Actualizado Instructor y Student
  • Agregado Person al modelo
  • Creado y actualizado migraciones
  • Probado la implementación

Pase al tutorial siguiente para obtener información sobre cómo controlar una serie de escenarios de Entity Framework relativamente avanzados.