Compartilhar via


Implementando herança com o Entity Framework em um aplicativo MVC ASP.NET (8 de 10)

por Tom Dykstra

O aplicativo Web de exemplo da Contoso University demonstra como criar ASP.NET aplicativos MVC 4 usando o Entity Framework 5 Code First e o Visual Studio 2012. Para obter informações sobre a série de tutoriais, consulte o primeiro tutorial da série.

Observação

Se você tiver um problema, não poderá resolve, baixe o capítulo concluído e tente reproduzir o problema. Geralmente, você pode encontrar a solução para o problema comparando seu código com o código concluído. Para obter alguns erros comuns e como resolvê-los, consulte Erros e soluções alternativas.

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

Na programação orientada a objetos, você pode usar a herança para eliminar o código redundante. 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.

Herança tabela por hierarquia versus tabela por tipo

Na programação orientada a objetos, você pode usar a herança para facilitar o trabalho com classes relacionadas. Por exemplo, as Instructor classes e Student no modelo de dados compartilham várias propriedades, o School que resulta em código redundante:

Capturas de tela que mostram as classes Aluno e Instrutor com códigos redundantes realçados.

Suponha que você deseje eliminar o código redundante para as propriedades compartilhadas pelas entidades Instructor e Student. Você pode criar uma Person classe base que contenha apenas essas propriedades compartilhadas e, em seguida, fazer com que as Instructor entidades e Student herdem dessa classe base, conforme mostrado na ilustração a seguir:

Captura de tela que mostra as classes Aluno e Instrutor derivadas da classe Person.

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 só podem ser aplicadas a instrutores (HireDate), algumas apenas aos alunos (EnrollmentDate), algumas a ambos (LastName, FirstName). Normalmente, você teria 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.

Captura de tela que mostra a estrutura de herança da classe de entidade Person.

Esse padrão de geração de uma estrutura de herança de entidade de uma única tabela de banco de dados é 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.

Captura de tela que mostra as novas tabelas de banco de dados Instrutor e Aluno derivadas da classe de entidade Pessoa.

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

Os padrões de herança TPH geralmente oferecem melhor desempenho no Entity Framework do que os padrões de herança TPT, pois os padrões TPT podem resultar em consultas de junção complexas. Este tutorial demonstra como implementar a herança TPH. Você fará isso executando as seguintes etapas:

  • Crie uma Person classe e altere as Instructor classes e Student para derivar de Person.
  • Adicione o código de mapeamento de modelo para banco de dados à classe de contexto do banco de dados.
  • Altere InstructorID e StudentID faça referências em todo o projeto para PersonID.

Criando a classe Person

Observação: você não poderá compilar o projeto depois de criar as classes abaixo até atualizar os controladores que usam essas classes.

Na pasta Modelos , 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
   {
      [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;
         }
      }
   }
}

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

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

Faça alterações semelhantes a Student.cs. A Student classe será semelhante ao seguinte exemplo:

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

Adicionando o tipo de entidade de pessoa ao modelo

Em SchoolContext.cs, adicione uma DbSet propriedade para o Person tipo de entidade:

public DbSet<Person> People { get; set; }

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 recriado, ele terá uma Person tabela no lugar das Student tabelas e Instructor .

Alterando InstructorID e StudentID para PersonID

Em SchoolContext.cs, na instrução de mapeamento Instructor-Course, altere MapRightKey("InstructorID") para MapRightKey("PersonID"):

modelBuilder.Entity<Course>()
    .HasMany(c => c.Instructors).WithMany(i => i.Courses)
    .Map(t => t.MapLeftKey("CourseID")
    .MapRightKey("PersonID")
    .ToTable("CourseInstructor"));

Essa alteração não é necessária; ele apenas altera o nome da coluna InstructorID na tabela de junção muitos para muitos. Se você deixou o nome como InstructorID, o aplicativo ainda funcionará corretamente. Aqui está o SchoolContext.cs concluído:

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

Em seguida, você precisa alterar InstructorID para PersonID e StudentID para PersonID em todo o projeto , exceto nos arquivos de migrações com carimbo de data/hora na pasta Migrações . Para fazer isso, você encontrará e abrirá apenas os arquivos que precisam ser alterados e, em seguida, executará uma alteração global nos arquivos abertos. O único arquivo na pasta Migrações que você deve alterar é Migrations\Configuration.cs.

  1. Importante

    Comece fechando todos os arquivos abertos no Visual Studio.

  2. Clique em Localizar e Substituir – Localizar todos os Arquivos no menu Editar e pesquise todos os arquivos no projeto que contêm InstructorID.

    Captura de tela que mostra a janela Localizar e Substituir. As caixas de seleção Instrutor ID, Projeto Atual, Correspondência e Correspondência de palavras inteiras e o botão Localizar Tudo estão realçados.

  3. Abra cada arquivo na janela Localizar Resultados, exceto os <arquivos de migração time-stamp>_.cs na pasta Migrações, clicando duas vezes em uma linha para cada arquivo.

    Captura de tela que mostra a janela Localizar Resultados. Os arquivos de migração de carimbo de data/hora são risados em vermelho.

  4. Abra a caixa de diálogo Substituir em Arquivos e altere Olhar para Todos os Documentos Abertos.

  5. Use a caixa de diálogo Substituir em Arquivos para alterar tudo InstructorID para PersonID.

    Captura de tela que mostra a janela Localizar e Substituir. A pessoa ID é inserida no campo Substituir por texto.

  6. Encontre todos os arquivos no projeto que contêm StudentID.

  7. Abra cada arquivo na janela Localizar Resultados, exceto os <arquivos de migração time-stamp>_*.cs na pasta Migrações, clicando duas vezes em uma linha para cada arquivo.

    Captura de tela que mostra a janela Localizar Resultados. Os arquivos de migração de carimbo de data/hora são risados.

  8. Abra a caixa de diálogo Substituir em Arquivos e altere Olhar para Todos os Documentos Abertos.

  9. Use a caixa de diálogo Substituir em Arquivos para alterar tudo StudentID para PersonID.

    Captura de tela que mostra a janela Localizar e Substituir. Substitua em Arquivos, Todas as Caixas de seleção Abrir Documentos, Correspondência e Corresponder palavra inteira e botão Substituir Tudo estão realçados.

  10. Compile o projeto.

(Observe que isso demonstra uma desvantagem do classnameID padrão para nomear chaves primárias. Se você tivesse nomeado a ID das chaves primárias sem prefixar o nome da classe, nenhuma renomeação seria necessária agora.)

Criar e atualizar um arquivo de migrações

No PMC (Console do Gerenciador de Pacotes), insira o seguinte comando:

Add-Migration Inheritance

Execute o Update-Database comando no PMC. O comando falhará neste momento porque temos dados existentes que as migrações não sabem como lidar. Você obterá o seguinte erro:

A instrução ALTER TABLE entrou em conflito com a restrição FOREIGN KEY "FK_dbo. Department_dbo. Person_PersonID". O conflito ocorreu no banco de dados "ContosoUniversity", tabela "dbo. Pessoa", coluna 'PersonID'.

Abrir Migrações< timestamp>_Inheritance.cs e substitua o Up método pelo seguinte código:

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

Execute o comando update-database novamente.

Observação

É possível obter outros erros ao migrar dados e fazer alterações de esquema. Se você receber erros de migração não puder resolve, poderá continuar com o tutorial alterando o cadeia de conexão no arquivo Web.config ou excluindo o banco de dados. A abordagem mais simples é renomear o banco de dados no arquivo Web.config . Por exemplo, altere o nome do banco de dados para CU_test conforme mostrado no exemplo a seguir:

<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" />

Com um novo banco de dados, não há dados a serem migrados e é muito mais provável que o update-database comando seja concluído sem erros. Para obter instruções sobre como excluir o banco de dados, consulte Como remover um banco de dados do Visual Studio 2012. Se você adotar essa abordagem para continuar com o tutorial, ignore a etapa de implantação no final deste tutorial, pois o site implantado receberá o mesmo erro quando executar as migrações automaticamente. Se você quiser solucionar um erro de migrações, o melhor recurso será um dos fóruns do Entity Framework ou StackOverflow.com.

Testando

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

No Servidor Explorer, expanda SchoolContext e, em seguida, Tabelas, e você vê que as tabelas Aluno e Instrutor foram substituídas por uma tabela Pessoa. Expanda a tabela Pessoa e você verá que ela tem todas as colunas que costumavam estar nas tabelas Aluno e Instrutor .

Captura de tela que mostra a janela Explorer do Servidor. As guias Conexões de Dados, Contexto Escolar e Tabelas são expandidas para mostrar a tabela Pessoa.

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.

Captura de tela que mostra a tabela Pessoa. O nome da coluna Discriminatório está realçado.

O diagrama a seguir ilustra a estrutura do novo banco de dados da Escola:

Captura de tela que mostra o diagrama de banco de dados da escola.

Resumo

A herança tabela por hierarquia agora foi implementada para as Personclasses , Studente Instructor . Para obter mais informações sobre essa e outras estruturas de herança, consulte Estratégias de mapeamento de herança no blog de Morteza Manavi. No próximo tutorial, você verá algumas maneiras de implementar o repositório e a unidade de padrões de trabalho.

Links para outros recursos do Entity Framework podem ser encontrados no mapa de conteúdo de acesso a dados do ASP.NET.