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:
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:
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.
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.
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 asInstructor
classes eStudent
para derivar dePerson
. - Adicione o código de mapeamento de modelo para banco de dados à classe de contexto do banco de dados.
- Altere
InstructorID
eStudentID
faça referências em todo o projeto paraPersonID
.
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.
-
Importante
Comece fechando todos os arquivos abertos no Visual Studio.
Clique em Localizar e Substituir – Localizar todos os Arquivos no menu Editar e pesquise todos os arquivos no projeto que contêm
InstructorID
.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.
Abra a caixa de diálogo Substituir em Arquivos e altere Olhar para Todos os Documentos Abertos.
Use a caixa de diálogo Substituir em Arquivos para alterar tudo
InstructorID
paraPersonID.
Encontre todos os arquivos no projeto que contêm
StudentID
.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.
Abra a caixa de diálogo Substituir em Arquivos e altere Olhar para Todos os Documentos Abertos.
Use a caixa de diálogo Substituir em Arquivos para alterar tudo
StudentID
paraPersonID
.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 .
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.
O diagrama a seguir ilustra a estrutura do novo banco de dados da Escola:
Resumo
A herança tabela por hierarquia agora foi implementada para as Person
classes , Student
e 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.