Tutorial: Implementar a herança - ASP.NET MVC com EF Core

No tutorial anterior, você tratou exceções de simultaneidade. Este tutorial mostrará como implementar a herança no modelo de dados.

Na programação orientada a objeto, você pode usar a herança para facilitar a reutilização de código. Neste tutorial, você alterará as classes Instructor e Student, de modo que elas derivem de uma classe base Person que contém propriedades, como LastName, comuns a instrutores e alunos. Você não adicionará nem alterará as páginas da Web, mas alterará uma parte do código, e essas alterações serão refletidas automaticamente no banco de dados.

Neste tutorial, você:

  • Mapeará a herança para o banco de dados
  • Criar a classe Person
  • Atualizará Instructor e Student
  • Adicionará Person ao modelo
  • Criará e atualizará migrações
  • Testará a implementação

Pré-requisitos

Mapeará a herança para o banco de dados

As classes Instructor e Student no modelo de dados Escola têm várias propriedades idênticas:

Student and Instructor classes

Suponha que você deseje eliminar o código redundante para as propriedades compartilhadas pelas entidades Instructor e Student. Ou você deseja escrever um serviço que pode formatar nomes sem se preocupar se o nome foi obtido de um instrutor ou um aluno. Você pode criar uma classe base Person que contém apenas essas propriedades compartilhadas e, em seguida, fazer com que as classes Instructor e Student herdem dessa classe base, conforme mostrado na seguinte ilustração:

Student and Instructor classes deriving from Person class

Há várias maneiras pelas quais essa estrutura de herança pode ser representada no banco de dados. Você pode ter uma tabela Person que inclui informações sobre alunos e instrutores em uma única tabela. Algumas das colunas podem se aplicar somente a instrutores (HireDate), algumas somente a alunos (EnrollmentDate) e outras a ambos (LastName, FirstName). Normalmente, você terá uma coluna discriminatória para indicar qual tipo cada linha representa. Por exemplo, a coluna discriminatória pode ter "Instrutor" para instrutores e "Aluno" para alunos.

Table-per-hierarchy example

Esse padrão de geração de uma estrutura de herança de entidade com base em uma tabela de banco de dados individual é chamado de herança TPH (tabela por hierarquia).

Uma alternativa é fazer com que o banco de dados se pareça mais com a estrutura de herança. Por exemplo, você pode ter apenas os campos de nome na tabela Person e ter tabelas Instructor e Student separadas com os campos de data.

Aviso

A tabela por tipo (TPT) não tem suporte no EF Core 3.x, no entanto, ela foi implementada no EF Core 5.0.

Table-per-type inheritance

Esse padrão de criação de uma tabela de banco de dados para cada classe de entidade é chamado de herança TPT (tabela por tipo).

Outra opção é mapear todos os tipos não abstratos para tabelas individuais. Todas as propriedades de uma classe, incluindo propriedades herdadas, são mapeadas para colunas da tabela correspondente. Esse padrão é chamado de herança TPC (Tabela por Classe Concreta). Se você tiver implementado a herança TPC para as classes Person, Student e Instructor, conforme mostrado anteriormente, as tabelas Student e Instructor não parecerão diferentes após a implementação da herança do que antes.

Em geral, os padrões de herança TPC e TPH oferecem melhor desempenho do que os padrões de herança TPT, porque os padrões TPT podem resultar em consultas de junção complexas.

Este tutorial demonstra como implementar a herança TPH. TPH é o único padrão de herança compatível com o Entity Framework Core. O que você fará é criar uma classe Person, alterar as classes Instructor e Student para que elas derivem de Person, adicionar a nova classe ao DbContext e criar uma migração.

Dica

Considere a possibilidade de salvar uma cópia do projeto antes de fazer as alterações a seguir. Em seguida, se você tiver problemas e precisar recomeçar, será mais fácil começar do projeto salvo, em vez de reverter as etapas executadas para este tutorial ou voltar ao início da série inteira.

Criar a classe Person

Na pasta Models, crie Person.cs e substitua o código de modelo pelo seguinte código:

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;
            }
        }
    }
}

Atualizará Instructor e Student

Em Instructor.cs, derive a classe Instructor da classe Person e remova os campos de nome e chave. O código será semelhante ao seguinte exemplo:

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; }
    }
}

Faça as mesmas alterações em 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; }
    }
}

Adicionará Person ao modelo

Adicionar o tipo de entidade Person ao SchoolContext.cs. As novas linhas são realçadas.

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 });
        }
    }
}

Isso é tudo o que o Entity Framework precisa para configurar a herança de tabela por hierarquia. Como você verá, quando o banco de dados for atualizado, ele terá uma tabela Pessoa no lugar das tabelas Aluno e Instrutor.

Criará e atualizará migrações

Salve as alterações e compile o projeto. Em seguida, abra a janela Comando na pasta do projeto e insira o seguinte comando:

dotnet ef migrations add Inheritance

Não execute o comando database update ainda. Esse comando resultará em perda de dados porque ele removerá a tabela Instrutor e renomeará a tabela Aluno como Pessoa. Você precisa fornecer o código personalizado para preservar os dados existentes.

Abra Migrations/<timestamp>_Inheritance.cs e substitua o método Up pelo seguinte código:

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 é responsável pelas seguintes tarefas de atualização de banco de dados:

  • Remove as restrições de chave estrangeira e índices que apontam para a tabela Aluno.

  • Renomeia a tabela Instrutor como Pessoa e faz as alterações necessárias para que ela armazene dados de Aluno:

  • Adiciona EnrollmentDate que permite valo nulo para os alunos.

  • Adiciona a coluna Discriminatória para indicar se uma linha refere-se a um aluno ou um instrutor.

  • Faz com que HireDate permita valor nulo, pois linhas de alunos não terão datas de contratação.

  • Adiciona um campo temporário que será usado para atualizar chaves estrangeiras que apontam para alunos. Quando você copiar os alunos para a tabela Person, eles receberão novos valores de chave primária.

  • Copia os dados da tabela Aluno para a tabela Pessoa. Isso faz com que os alunos recebam novos valores de chave primária.

  • Corrige valores de chave estrangeira que apontam para alunos.

  • Recria restrições de chave estrangeira e índices, agora apontando-os para a tabela Person.

(Se você tiver usado o GUID, em vez de inteiro como o tipo de chave primária, os valores de chave primária dos alunos não precisarão ser alterados e várias dessas etapas poderão ser omitidas.)

Executar o comando database update:

dotnet ef database update

(Em um sistema de produção, você fará as alterações correspondentes no método Down, caso já tenha usado isso para voltar à versão anterior do banco de dados. Para este tutorial, você não usará o método Down.)

Observação

É possível receber outros erros ao fazer alterações de esquema em um banco de dados que contém dados existentes. Se você receber erros de migração que não consegue resolver, altere o nome do banco de dados na cadeia de conexão ou exclua o banco de dados. Com um novo banco de dados, não há nenhum dado a ser migrado e o comando de atualização de banco de dados terá uma probabilidade maior de ser concluído sem erros. Para excluir o banco de dados, use o SSOX ou execute o comando database drop da CLI.

Testará a implementação

Execute o aplicativo e teste várias páginas. Tudo funciona da mesma maneira que antes.

No Pesquisador de Objetos do SQL Server, expanda Data Connections/SchoolContext e, em seguida, Tabelas e você verá que as tabelas Aluno e Instrutor foram substituídas por uma tabela Pessoa. Abra o designer de tabela Pessoa e você verá que ela contém todas as colunas que costumavam estar nas tabelas Aluno e Instrutor.

Person table in SSOX

Clique com o botão direito do mouse na tabela Person e, em seguida, clique em Mostrar Dados da Tabela para ver a coluna discriminatória.

Person table in SSOX - table data

Obter o código

Baixe ou exiba o aplicativo concluído.

Recursos adicionais

Para obter mais informações sobre a herança no Entity Framework Core, consulte Herança.

Próximas etapas

Neste tutorial, você:

  • Mapeou a herança para o banco de dados
  • Criou a classe Person
  • Atualizou Instructor e Student
  • Adicionou Person ao modelo
  • Criou e atualizou migrações
  • Testou a implementação

Vá para o próximo tutorial para saber como lidar com vários cenários relativamente avançados do Entity Framework.