Tutorial: Criar um modelo de dados mais complexo para um aplicativo MVC ASP.NET
Nos tutoriais anteriores, você trabalhou com um modelo de dados simples composto por três entidades. Neste tutorial, você adiciona mais entidades e relações e personaliza o modelo de dados especificando regras de formatação, validação e mapeamento de banco de dados. Este artigo mostra duas maneiras de personalizar o modelo de dados: adicionando atributos a classes de entidade e adicionando código à classe de contexto do banco de dados.
Quando terminar, as classes de entidade formarão o modelo de dados concluído mostrado na seguinte ilustração:
Neste tutorial, você:
- Personalizar o modelo de dados
- Atualizar entidade Student
- Criar a entidade Instructor
- Criar a entidade OfficeAssignment
- Modificar a entidade Course
- Criar a entidade Department
- Modificar a entidade Enrollment
- Adicionar código ao contexto do banco de dados
- Propagar o banco de dados com dados de teste
- Adicionar uma migração
- Atualizar o banco de dados
Pré-requisitos
Personalizar o modelo de dados
Nesta seção, você verá como personalizar o modelo de dados usando atributos que especificam formatação, validação e regras de mapeamento de banco de dados. Em seguida, em várias das seções a seguir, você criará o modelo de dados completo School
adicionando atributos às classes que você já criou e criando novas classes para os tipos de entidade restantes no modelo.
O atributo DataType
Para datas de registro de alunos, todas as páginas da Web atualmente exibem a hora junto com a data, embora tudo o que você deseje exibir nesse campo seja a data. Usando atributos de anotação de dados, você pode fazer uma alteração de código que corrigirá o formato de exibição em cada exibição que mostra os dados. Para ver um exemplo de como fazer isso, você adicionará um atributo à propriedade EnrollmentDate
na classe Student
.
Em Models\Student.cs, adicione uma using
instrução para o System.ComponentModel.DataAnnotations
namespace e adicione DataType
atributos e DisplayFormat
à EnrollmentDate
propriedade, conforme mostrado no exemplo a seguir:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
O atributo DataType é usado para especificar um tipo de dados mais específico do que o tipo intrínseco do banco de dados. Nesse caso, apenas desejamos acompanhar a data, não a data e a hora. A Enumeração de Tipo de Dados fornece muitos tipos de dados, como Data, Hora, Número de Telefone, Moeda, Endereço de Email e muito mais. O atributo DataType
também pode permitir que o aplicativo forneça automaticamente recursos específicos a um tipo. Por exemplo, um mailto:
link pode ser criado para DataType.EmailAddress e um seletor de data pode ser fornecido para DataType.Date em navegadores que dão suporte a HTML5. Os atributos DataType emitem atributos HTML 5 data- (pronuncia-se data dash) que os navegadores HTML 5 podem entender. Os atributos DataType não fornecem nenhuma validação.
DataType.Date
não especifica o formato da data exibida. Por padrão, o campo de dados é exibido de acordo com os formatos padrão com base no CultureInfo do servidor.
O atributo DisplayFormat
é usado para especificar explicitamente o formato de data:
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
A ApplyFormatInEditMode
configuração especifica que a formatação especificada também deve ser aplicada quando o valor é exibido em uma caixa de texto para edição. (Talvez você não queira isso para alguns campos — por exemplo, para valores de moeda, talvez não queira o símbolo de moeda na caixa de texto para edição.)
Você pode usar o atributo DisplayFormat sozinho, mas geralmente é uma boa ideia usar o atributo DataType também. O DataType
atributo transmite a semântica dos dados em vez de como renderizá-los em uma tela e fornece os seguintes benefícios que você não obtém comDisplayFormat
:
- O navegador pode habilitar os recursos do HTML5 (por exemplo, mostrar um controle de calendário, o símbolo de moeda apropriado à localidade, links de email, uma validação de entrada do lado do cliente, etc.).
- Por padrão, o navegador renderizará os dados usando o formato correto com base em sua localidade.
- O atributo DataType pode permitir que o MVC escolha o modelo de campo correto para renderizar os dados (o DisplayFormat usa o modelo de cadeia de caracteres). Para obter mais informações, consulte os modelos ASP.NET MVC 2 de Brad Wilson. (Embora escrito para MVC 2, este artigo ainda se aplica à versão atual do ASP.NET MVC.)
Se você usar o DataType
atributo com um campo de data, também precisará especificar o DisplayFormat
atributo para garantir que o campo seja renderizado corretamente nos navegadores Chrome. Para obter mais informações, consulte este tópico do StackOverflow.
Para obter mais informações sobre como lidar com outros formatos de data no MVC, acesse Introdução ao MVC 5: Examinando os métodos de edição e o Modo de Exibição de Edição e pesquise na página por "internacionalização".
Execute a página Índice de Alunos novamente e observe que os horários não são mais exibidos para as datas de inscrição. O mesmo será verdadeiro para qualquer vista que use o Student
modelo.
O StringLengthAttribute
Você também pode especificar regras de validação de dados e mensagens de erro de validação usando atributos. O atributo StringLength define o comprimento máximo no banco de dados e fornece validação do lado do cliente e do lado do servidor para ASP.NET MVC. Você também pode especificar o tamanho mínimo da cadeia de caracteres nesse atributo, mas o valor mínimo não tem nenhum impacto sobre o esquema de banco de dados.
Suponha que você deseje garantir que os usuários não insiram mais de 50 caracteres em um nome. Para adicionar essa limitação, adicione atributos StringLength às LastName
propriedades and FirstMidName
, conforme mostrado no exemplo a seguir:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
O atributo StringLength não impedirá que um usuário insira espaço em branco para um nome. Você pode usar o atributo RegularExpression para aplicar restrições à entrada. Por exemplo, o código a seguir requer que o primeiro caractere seja maiúsculo e os caracteres restantes sejam alfabéticos:
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
O atributo MaxLength fornece funcionalidade semelhante ao atributo StringLength , mas não fornece validação do lado do cliente.
Execute o aplicativo e clique na guia Alunos. Você recebe o seguinte erro:
O modelo que dá suporte ao contexto 'SchoolContext' foi alterado desde que o banco de dados foi criado. Considere usar as Migrações do Code First para atualizar o banco de dados (https://go.microsoft.com/fwlink/?LinkId=238269).
O modelo de banco de dados foi alterado de uma forma que requer uma alteração no esquema de banco de dados, e o Entity Framework detectou isso. Você usará migrações para atualizar o esquema sem perder os dados adicionados ao banco de dados usando a interface do usuário. Se você alterou os Seed
dados criados pelo método, eles serão alterados de volta ao seu estado original devido ao método AddOrUpdate que você está usando no Seed
método. (AddOrUpdate é equivalente a uma operação "upsert" da terminologia do banco de dados.)
No PMC (Console do Gerenciador de Pacotes), Insira os seguintes comandos:
add-migration MaxLengthOnNames
update-database
O add-migration
comando cria um arquivo chamado <timeStamp>_MaxLengthOnNames.cs. Esse arquivo contém o código no método Up
que atualizará o banco de dados para que ele corresponda ao modelo de dados atual. O comando update-database
executou esse código.
O carimbo de data/hora anexado ao nome do arquivo de migrações é usado pelo Entity Framework para ordenar as migrações. Você pode criar várias migrações antes de executar o update-database
comando e, em seguida, todas as migrações são aplicadas na ordem em que foram criadas.
Execute a página Criar e insira um dos nomes com mais de 50 caracteres. Quando você clica em Criar, a validação do lado do cliente mostra uma mensagem de erro: O campo LastName deve ser uma cadeia de caracteres com um comprimento máximo de 50.
O atributo de coluna
Você também pode usar atributos para controlar como as classes e propriedades são mapeadas para o banco de dados. Suponha que você tenha usado o nome FirstMidName
para o campo de nome porque o campo também pode conter um sobrenome. Mas você deseja que a coluna do banco de dados seja nomeada FirstName
, pois os usuários que escreverão consultas ad hoc no banco de dados estão acostumados com esse nome. Para fazer esse mapeamento, use o atributo Column
.
O atributo Column
especifica que quando o banco de dados for criado, a coluna da tabela Student
que é mapeada para a propriedade FirstMidName
será nomeada FirstName
. Em outras palavras, quando o código se referir a Student.FirstMidName
, os dados serão obtidos ou atualizados na coluna FirstName
da tabela Student
. Se você não especificar nomes de coluna, eles receberão o mesmo nome que o nome da propriedade.
No arquivo Student.cs, adicione uma using
instrução para System.ComponentModel.DataAnnotations.Schema e adicione o atributo de nome da coluna à FirstMidName
propriedade, conforme mostrado no código realçado a seguir:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
A adição do atributo Column altera o modelo que dá suporte ao SchoolContext, portanto, ele não corresponderá ao banco de dados. Insira os seguintes comandos no PMC para criar outra migração:
add-migration ColumnFirstName
update-database
No Gerenciador de Servidores, abra o designer de tabela Aluno clicando duas vezes na tabela Aluno .
A imagem a seguir mostra o nome da coluna original como era antes de você aplicar as duas primeiras migrações. Além do nome da coluna mudar de FirstMidName
para FirstName
, as duas colunas de nome foram alteradas de MAX
comprimento para 50 caracteres.
Você também pode fazer alterações no mapeamento do banco de dados usando a API Fluent, como verá mais adiante neste tutorial.
Observação
Se você tentar compilar antes de concluir a criação de todas as classes de entidade nas seções a seguir, poderá receber erros do compilador.
Atualizar entidade Student
Em Models\Student.cs, substitua o código adicionado anteriormente pelo código a seguir. As alterações são realçadas.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
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; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
O atributo obrigatório
O atributo Obrigatório torna as propriedades do nome campos obrigatórios. O Required attribute
não é necessário para tipos de valor como DateTime, int, double e float. Os tipos de valor não podem receber um valor nulo, portanto, eles são tratados inerentemente como campos obrigatórios.
O atributo Required
precisa ser usado com MinimumLength
para que MinimumLength
seja imposto.
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength
e Required
permitem que o espaço em branco atenda à validação. Use o RegularExpression
atributo para controle total sobre a cadeia de caracteres.
O atributo de exibição
O atributo Display
especifica que a legenda para as caixas de texto deve ser "Nome", "Sobrenome", "Nome Completo" e "Data de Registro", em vez do nome de a propriedade em cada instância (que não tem nenhum espaço entre as palavras).
A propriedade calculada FullName
FullName
é uma propriedade calculada que retorna um valor criado pela concatenação de duas outras propriedades. Portanto, ele tem apenas um get
acessador e nenhuma FullName
coluna será gerada no banco de dados.
Criar a entidade Instructor
Crie Models\Instructor.cs, substituindo o código do modelo pelo seguinte código:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor
{
public int ID { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
}
Observe que várias propriedades são iguais nas entidades Student
e Instructor
. No tutorial Implementando a herança mais adiante nesta série, você refatorará esse código para eliminar a redundância.
Você pode colocar vários atributos em uma linha, portanto, também pode escrever a classe de instrutor da seguinte maneira:
public class Instructor
{
public int ID { get; set; }
[Display(Name = "Last Name"),StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
[Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)]
public string FirstMidName { get; set; }
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
As propriedades de navegação Courses e OfficeAssignment
As propriedades Courses
e OfficeAssignment
são propriedades de navegação. Como foi explicado anteriormente, eles normalmente são definidos como virtuais para que possam aproveitar um recurso do Entity Framework chamado carregamento lento. Além disso, se uma propriedade de navegação puder conter várias entidades, seu tipo deverá implementar a Interface T ICollection<.> Por exemplo , IList<T> se qualifica, mas não IEnumerable<T> porque IEnumerable<T>
não implementa Add.
Um instrutor pode ministrar qualquer número de cursos, portanto Courses
, é definido como uma coleção de Course
entidades.
public virtual ICollection<Course> Courses { get; set; }
Nossas regras de negócios afirmam que um instrutor só pode ter no máximo um escritório, portanto OfficeAssignment
, é definido como uma única OfficeAssignment
entidade (que pode ser null
se nenhum escritório for atribuído).
public virtual OfficeAssignment OfficeAssignment { get; set; }
Criar a entidade OfficeAssignment
Crie Modelos\OfficeAssignment.cs com o seguinte código:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public virtual Instructor Instructor { get; set; }
}
}
Crie o projeto, que salva suas alterações e verifica se você não cometeu nenhum erro de copiar e colar que o compilador possa detectar.
O atributo de chave
Há uma relação “um para zero ou um” entre as entidades Instructor
e OfficeAssignment
. Uma atribuição de escritório existe apenas em relação ao instrutor ao qual ela é atribuída e, portanto, sua chave primária também é a chave estrangeira da entidade Instructor
. Mas o Entity Framework não pode reconhecer InstructorID
automaticamente como a chave primária dessa entidade porque seu nome não segue a ID
convenção de nomenclatura ou classnameID
. Portanto, o atributo Key
é usado para identificá-la como a chave:
[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }
Você também pode usar o Key
atributo se a entidade tiver sua própria chave primária, mas quiser nomear a propriedade com algo diferente de classnameID
ou ID
. Por padrão, o EF trata a chave como não gerada pelo banco de dados porque a coluna é para uma relação de identificação.
O atributo ForeignKey
Quando há uma relação um-para-zero-ou-um ou uma relação um-para-um entre duas entidades (como entre OfficeAssignment
e Instructor
), o EF não consegue descobrir qual extremidade da relação é a principal e qual extremidade é dependente. As relações um-para-um têm uma propriedade de navegação de referência em cada classe para a outra classe. O atributo ForeignKey pode ser aplicado à classe dependente para estabelecer a relação. Se você omitir o atributo ForeignKey, receberá o seguinte erro ao tentar criar a migração:
Não é possível determinar a extremidade principal de uma associação entre os tipos 'ContosoUniversity.Models.OfficeAssignment' e 'ContosoUniversity.Models.Instructor'. A extremidade principal dessa associação deve ser configurada explicitamente usando a API fluente de relacionamento ou anotações de dados.
Mais adiante no tutorial, você verá como configurar essa relação com a API fluente.
A propriedade de navegação do instrutor
A Instructor
entidade tem uma propriedade de navegação anulável OfficeAssignment
(porque um instrutor pode não ter uma atribuição de escritório) e a OfficeAssignment
entidade tem uma propriedade de navegação não anulável Instructor
(porque uma atribuição de escritório não pode existir sem um instrutor – InstructorID
não é anulável). Quando uma Instructor
entidade tiver uma entidade relacionada OfficeAssignment
, cada entidade terá uma referência à outra em sua propriedade de navegação.
Você pode colocar um [Required]
atributo na propriedade de navegação Instructor para especificar que deve haver um instrutor relacionado, mas não precisa fazer isso porque a chave estrangeira InstructorID (que também é a chave para essa tabela) não permite valor nulo.
Modificar a entidade Course
Em Models\Course.cs, substitua o código adicionado anteriormente pelo seguinte código:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Title { get; set; }
[Range(0, 5)]
public int Credits { get; set; }
public int DepartmentID { get; set; }
public virtual Department Department { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
public virtual ICollection<Instructor> Instructors { get; set; }
}
}
A entidade course tem uma propriedade DepartmentID
de chave estrangeira que aponta para a entidade relacionada Department
e tem uma Department
propriedade de navegação. O Entity Framework não exige que você adicione uma propriedade de chave estrangeira ao modelo de dados quando você tem uma propriedade de navegação para uma entidade relacionada. O EF cria automaticamente chaves estrangeiras no banco de dados sempre que necessário. No entanto, ter a chave estrangeira no modelo de dados pode tornar as atualizações mais simples e mais eficientes. Por exemplo, quando você busca uma entidade de curso para editar, a Department
entidade é nula se você não carregá-la, portanto, ao atualizar a entidade de curso, você teria que primeiro buscar a Department
entidade. Quando a propriedade de chave estrangeira DepartmentID
estiver incluída no modelo de dados, você não precisará buscar a entidade Department
antes da atualização.
O atributo DatabaseGenerated
O atributo DatabaseGenerated com o parâmetro None na CourseID
propriedade especifica que os valores de chave primária são fornecidos pelo usuário em vez de gerados pelo banco de dados.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
Por padrão, o Entity Framework pressupõe que os valores de chave primária sejam gerados pelo banco de dados. É isso que você quer na maioria dos cenários. No entanto, para entidades Course
, você usará um número de curso especificado pelo usuário como uma série de 1.000 de um departamento, uma série de 2.000 para outro departamento e assim por diante.
Propriedades de chave estrangeira e navegação
As propriedades de navegação e de chave estrangeira na entidade Course
refletem as seguintes relações:
Um curso é atribuído a um departamento e, portanto, há uma propriedade de chave estrangeira
DepartmentID
e de navegaçãoDepartment
pelas razões mencionadas acima.public int DepartmentID { get; set; } public virtual Department Department { get; set; }
Um curso pode ter qualquer quantidade de estudantes inscritos; portanto, a propriedade de navegação
Enrollments
é uma coleção:public virtual ICollection<Enrollment> Enrollments { get; set; }
Um curso pode ser ministrado por vários instrutores; portanto, a propriedade de navegação
Instructors
é uma coleção:public virtual ICollection<Instructor> Instructors { get; set; }
Criar a entidade Department
Crie Modelos\Department.cs com o seguinte código:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength=3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
public virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
}
O atributo de coluna
Anteriormente, você usava o atributo Column para alterar o mapeamento do nome da coluna. No código da Department
entidade, o Column
atributo está sendo usado para alterar o mapeamento do tipo de dados SQL para que a coluna seja definida usando o tipo money do SQL Server no banco de dados:
[Column(TypeName="money")]
public decimal Budget { get; set; }
O mapeamento de coluna geralmente não é necessário, pois o Entity Framework geralmente escolhe o tipo de dados SQL Server apropriado com base no tipo CLR definido para a propriedade. O tipo decimal
CLR é mapeado para um tipo decimal
SQL Server. Mas, nesse caso, você sabe que a coluna conterá valores monetários e o tipo de dados money é mais apropriado para isso. Para obter mais informações sobre tipos de dados CLR e como eles correspondem aos tipos de dados do SQL Server, consulte SqlClient para Entity FrameworkTypes.
Propriedades de chave estrangeira e navegação
As propriedades de navegação e de chave estrangeira refletem as seguintes relações:
Um departamento pode ou não ter um administrador, e um administrador é sempre um instrutor. Portanto, a
InstructorID
propriedade é incluída como a chave estrangeira para aInstructor
entidade e um ponto de interrogação é adicionado após a designação deint
tipo para marcar a propriedade como anulável. A propriedade de navegação é nomeadaAdministrator
, mas contém umaInstructor
entidade:public int? InstructorID { get; set; } public virtual Instructor Administrator { get; set; }
Um departamento pode ter muitos cursos, portanto, há uma
Courses
propriedade de navegação:public virtual ICollection<Course> Courses { get; set; }
Observação
Por convenção, o Entity Framework habilita a exclusão em cascata para chaves estrangeiras que não permitem valor nulo e em relações muitos para muitos. Isso pode resultar em regras de exclusão em cascata circular, que causará uma exceção quando você tentar adicionar uma migração. Por exemplo, se você não definiu a
Department.InstructorID
propriedade como anulável, receberá a seguinte mensagem de exceção: "A relação referencial resultará em uma referência cíclica que não é permitida". Se suas regras de negócios exigissemInstructorID
que a propriedade não permitisse valor nulo, você teria que usar a seguinte instrução de API fluente para desabilitar a exclusão em cascata na relação:
modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);
Modificar a entidade Enrollment
Em Modelos\Enrollment.cs, substitua o código adicionado anteriormente pelo seguinte código
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }
public virtual Course Course { get; set; }
public virtual Student Student { get; set; }
}
}
Propriedades de chave estrangeira e navegação
As propriedades de navegação e de chave estrangeira refletem as seguintes relações:
Um registro destina-se a um único curso e, portanto, há uma propriedade de chave estrangeira
CourseID
e uma propriedade de navegaçãoCourse
:public int CourseID { get; set; } public virtual Course Course { get; set; }
Um registro destina-se a um único aluno e, portanto, há uma propriedade de chave estrangeira
StudentID
e uma propriedade de navegaçãoStudent
:public int StudentID { get; set; } public virtual Student Student { get; set; }
Relações muitos para muitos
Há uma relação muitos para muitos entre as entidades Student
e Course
, e entidade Enrollment
funciona como uma tabela de junção muitos para muitos com conteúdo no banco de dados. Isso significa que a tabela contém dados adicionais além de Enrollment
chaves estrangeiras para as tabelas unidas (neste caso, uma chave primária e uma Grade
propriedade).
A ilustração a seguir mostra a aparência dessas relações em um diagrama de entidades. (Este diagrama foi gerado usando o Entity Framework Power Tools; a criação do diagrama não faz parte do tutorial, está apenas sendo usada aqui como uma ilustração.)
Cada linha de relação tem um 1 em uma extremidade e um asterisco (*) na outra, indicando uma relação um para muitos.
Se a tabela Enrollment
não incluir informações de nota, ela apenas precisará conter as duas chaves estrangeiras CourseID
e StudentID
. Nesse caso, ele corresponderia a uma tabela de junção muitos para muitos sem carga (ou uma tabela de junção pura) no banco de dados, e você não precisaria criar uma classe de modelo para ela. As Instructor
entidades e Course
têm esse tipo de relacionamento muitos-para-muitos e, como você pode ver, não há classe de entidade entre elas:
No entanto, uma tabela de junção é necessária no banco de dados, conforme mostrado no diagrama de banco de dados a seguir:
O Entity Framework cria automaticamente a CourseInstructor
tabela e você a lê e atualiza indiretamente lendo e atualizando as Instructor.Courses
propriedades de navegação and Course.Instructors
.
Diagrama de relacionamento da entidade
A ilustração a seguir mostra o diagrama criado pelo Entity Framework Power Tools para o modelo Escola concluído.
Além das linhas de relacionamento muitos para muitos (* para *) e as linhas de relacionamento um-para-muitos (1 para *), você pode ver aqui a linha de relacionamento um-para-zero-ou-um (1 a 0..1) entre as Instructor
entidades e OfficeAssignment
e a linha de relacionamento zero-ou-um-para-muitos (0..1 a *) entre as entidades Instrutor e Departamento.
Adicionar código ao contexto do banco de dados
Em seguida, você adicionará as novas entidades à SchoolContext
classe e personalizará parte do mapeamento usando chamadas de API fluentes. A API é "fluente" porque geralmente é usada pela cadeia de caracteres de uma série de chamadas de método em uma única instrução, como no exemplo a seguir:
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("InstructorID")
.ToTable("CourseInstructor"));
Neste tutorial, você usará a API fluente apenas para mapeamento de banco de dados que não pode ser feito com atributos. No entanto, você também pode usar a API fluente para especificar a maioria das regras de formatação, validação e mapeamento que pode ser feita por meio de atributos. Alguns atributos como MinimumLength
não podem ser aplicados com a API fluente. Como mencionado anteriormente, MinimumLength
não altera o esquema, apenas aplica uma regra de validação do lado do cliente e do servidor
Alguns desenvolvedores preferem usar a API fluente exclusivamente para que possam manter suas classes de entidade "limpas". Combine atributos e a API fluente se desejar. Além disso, há algumas personalizações que podem ser feitas apenas com a API fluente, mas em geral, a prática recomendada é escolher uma dessas duas abordagens e usar isso com o máximo de consistência possível.
Para adicionar as novas entidades ao modelo de dados e executar o mapeamento de banco de dados que você não fez usando atributos, substitua o código em DAL\SchoolContext.cs pelo seguinte código:
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; }
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("InstructorID")
.ToTable("CourseInstructor"));
}
}
}
A nova instrução no método OnModelCreating configura a tabela de junção muitos para muitos:
Para a relação muitos-para-muitos entre as
Instructor
entidades eCourse
, o código especifica os nomes de tabela e coluna para a tabela de junção. O Code First pode configurar a relação muitos-para-muitos para você sem esse código, mas se você não chamá-la, obterá nomes padrão, comoInstructorInstructorID
para aInstructorID
coluna.modelBuilder.Entity<Course>() .HasMany(c => c.Instructors).WithMany(i => i.Courses) .Map(t => t.MapLeftKey("CourseID") .MapRightKey("InstructorID") .ToTable("CourseInstructor"));
O código a seguir fornece um exemplo de como você poderia ter usado a API fluente em vez de atributos para especificar a relação entre as Instructor
entidades e OfficeAssignment
:
modelBuilder.Entity<Instructor>()
.HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);
Para obter informações sobre o que as instruções de "API fluente" estão fazendo nos bastidores, consulte a postagem no blog da API fluente.
Propagar o banco de dados com dados de teste
Substitua o código no arquivo Migrations\Configuration.cs pelo código a seguir para fornecer dados de semente para as novas entidades que você criou.
namespace ContosoUniversity.Migrations
{
using ContosoUniversity.Models;
using ContosoUniversity.DAL;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(SchoolContext context)
{
var students = new List<Student>
{
new Student { FirstMidName = "Carson", LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2010-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice",
EnrollmentDate = DateTime.Parse("2011-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2005-09-01") }
};
students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
context.SaveChanges();
var instructors = new List<Instructor>
{
new Instructor { FirstMidName = "Kim", LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11") },
new Instructor { FirstMidName = "Fadi", LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06") },
new Instructor { FirstMidName = "Roger", LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01") },
new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15") },
new Instructor { FirstMidName = "Roger", LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12") }
};
instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
context.SaveChanges();
var departments = new List<Department>
{
new Department { Name = "English", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Abercrombie").ID },
new Department { Name = "Mathematics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID },
new Department { Name = "Engineering", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Harui").ID },
new Department { Name = "Economics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID }
};
departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
context.SaveChanges();
var courses = new List<Course>
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 1045, Title = "Calculus", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 2021, Title = "Composition", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 2042, Title = "Literature", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();
var officeAssignments = new List<OfficeAssignment>
{
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
Location = "Smith 17" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
Location = "Gowan 27" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
Location = "Thompson 304" },
};
officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s));
context.SaveChanges();
AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
AddOrUpdateInstructor(context, "Chemistry", "Harui");
AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");
AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
AddOrUpdateInstructor(context, "Trigonometry", "Harui");
AddOrUpdateInstructor(context, "Composition", "Abercrombie");
AddOrUpdateInstructor(context, "Literature", "Abercrombie");
context.SaveChanges();
var enrollments = new List<Enrollment>
{
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").ID,
CourseID = courses.Single(c => c.Title == "Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Justice").ID,
CourseID = courses.Single(c => c.Title == "Literature").CourseID,
Grade = Grade.B
}
};
foreach (Enrollment e in enrollments)
{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
{
var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
if (inst == null)
crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
}
}
}
Como você viu no primeiro tutorial, a maior parte desse código simplesmente atualiza ou cria novos objetos de entidade e carrega dados de exemplo em propriedades conforme necessário para teste. No entanto, observe como a Course
entidade, que tem uma relação muitos-para-muitos com a Instructor
entidade, é tratada:
var courses = new List<Course>
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
Instructors = new List<Instructor>()
},
...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();
Ao criar um Course
objeto, você inicializa a Instructors
propriedade de navegação como uma coleção vazia usando o código Instructors = new List<Instructor>()
. Isso possibilita adicionar Instructor
entidades relacionadas a isso Course
usando o Instructors.Add
método. Se você não criasse uma lista vazia, não seria possível adicionar essas relações, pois a Instructors
propriedade seria nula e não teria um Add
método. Você também pode adicionar a inicialização da lista ao construtor.
Adicionar uma migração
No PMC, digite o add-migration
comando (não execute o update-database
comando ainda):
add-Migration ComplexDataModel
Se você tiver tentado executar o comando update-database
neste ponto (não faça isso ainda), receberá o seguinte erro:
A instrução ALTER TABLE entrou em conflito com a restrição FOREIGN KEY "FK_dbo.Course_dbo.Department_DepartmentID". O conflito ocorreu no banco de dados "ContosoUniversity", tabela "dbo.Departamento", coluna 'DepartmentID'.
Às vezes, quando você executa migrações com dados existentes, precisa inserir dados de stub no banco de dados para atender às restrições de chave estrangeira, e é isso que você precisa fazer agora. O código gerado no método ComplexDataModel Up
adiciona uma chave estrangeira não anulável DepartmentID
à Course
tabela. Como já há linhas na Course
tabela quando o código é executado, a operação falhará porque o AddColumn
SQL Server não sabe qual valor colocar na coluna que não pode ser nula. Portanto, tem que alterar o código para dar à nova coluna um valor padrão e criar um departamento de stub chamado "Temp" para atuar como o departamento padrão. Como resultado, as linhas existentes Course
serão todas relacionadas ao departamento "Temp" após a execução do Up
método. Você pode relacioná-los com os departamentos corretos no Seed
método.
Edite o arquivo _ComplexDataModel.cs carimbo <de data/hora>, comente a linha de código que adiciona a coluna DepartmentID à tabela Course e adicione o seguinte código realçado (a linha comentada também é destacada):
CreateTable(
"dbo.CourseInstructor",
c => new
{
CourseID = c.Int(nullable: false),
InstructorID = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.CourseID, t.InstructorID })
.ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
.ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
.Index(t => t.CourseID)
.Index(t => t.InstructorID);
// Create a department for course to point to.
Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// default value for FK points to department created above.
AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1));
//AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));
AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));
Quando o Seed
método for executado, ele inserirá linhas na Department
tabela e relacionará as linhas existentes Course
a essas novas Department
linhas. Se você não tiver adicionado nenhum curso na interface do usuário, não precisará mais do departamento "Temporário" ou do valor padrão na Course.DepartmentID
coluna. Para permitir a possibilidade de que alguém possa ter adicionado cursos usando o aplicativo, você também deseja atualizar o código do Seed
método para garantir que todas as Course
linhas (não apenas as inseridas por execuções anteriores do Seed
método) tenham valores válidos DepartmentID
antes de remover o valor padrão da coluna e excluir o departamento "Temp".
Atualizar o banco de dados
Depois de terminar de editar o arquivo _ComplexDataModel.cs carimbo <de data/hora>, insira o update-database
comando no PMC para executar a migração.
update-database
Observação
É possível obter outros erros ao migrar dados e fazer alterações de esquema. 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. A abordagem mais simples é renomear o banco de dados no arquivo Web.config . O exemplo a seguir mostra o nome alterado para_Test:
<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;Integrated Security=SSPI;"
providerName="System.Data.SqlClient" />
Com um novo banco de dados, não há dados para migrar 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 descartar um banco de dados do Visual Studio 2012.
Se isso falhar, outra coisa que você pode tentar é reinicializar o banco de dados digitando o seguinte comando no PMC:
update-database -TargetMigration:0
Abra o banco de dados no Gerenciador de Servidores como você fez anteriormente e expanda o nó Tabelas para ver se todas as tabelas foram criadas. (Se você ainda tem Gerenciador de Servidores aberto a partir do horário anterior, clique no botão Atualizar .)
Você não criou uma classe de modelo para a CourseInstructor
tabela. Conforme explicado anteriormente, esta é uma tabela de junção para a relação muitos-para-muitos entre as Instructor
entidades e Course
.
Clique com o botão direito do mouse na CourseInstructor
tabela e selecione Mostrar Dados da Tabela para verificar se ela contém dados como resultado das entidades adicionadas à Course.Instructors
propriedade de Instructor
navegação.
Obter o código
Recursos adicionais
Links para outros recursos do Entity Framework podem ser encontrados no ASP.NET Acesso a Dados – Recursos Recomendados.
Próximas etapas
Neste tutorial, você:
- Personalizou o modelo de dados
- Entidade Student atualizada
- Criou a entidade Instructor
- Criou a entidade OfficeAssignment
- Modificou a entidade Course
- Criou a entidade Departamento
- Modificou a entidade Enrollment
- Adicionado código ao contexto do banco de dados
- Propagou o banco de dados com os dados de teste
- Adicionou uma migração
- Atualizou o banco de dados
Avance para o próximo artigo para saber como ler e exibir dados relacionados que o Entity Framework carrega nas propriedades de navegação.