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 a 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 DataType fornece muitos tipos de dados, como Data, Hora, PhoneNumber, Conversor de Moedas, EmailAddress 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 de dados HTML 5 ( traço de dados pronunciado) 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 você não queira o símbolo de moeda na caixa de texto para edição.)
Você pode usar o atributo DisplayFormat por si só, 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 com DisplayFormat
:
- 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á dados usando o formato correto com base na sua localidade.
- O atributo DataType pode permitir que o MVC escolha o modelo de campo certo para renderizar os dados (o DisplayFormat usa o modelo de cadeia de caracteres). Para obter mais informações, consulte Modelos de 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, precisará especificar o DisplayFormat
atributo também para garantir que o campo seja renderizado corretamente em navegadores Chrome. Para obter mais informações, consulte este thread 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 Editar Exibição e pesquise na página para "internacionalização".
Execute a página Índice do Aluno 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 exibição 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 e 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 dados que foram criados pelo Seed
método , que serão alterados de volta para 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 nome 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 column
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 de coluna à FirstMidName
propriedade , conforme mostrado no seguinte código realçado:
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
Em Servidor Explorer, abra o designer de tabela Student clicando duas vezes na tabela Student.
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 que muda de FirstMidName
para FirstName
, as duas colunas de nome foram alteradas de MAX
comprimento para 50 caracteres.
Você também pode fazer alterações de mapeamento de banco de dados usando a API fluente, como você 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 a entidade Student
Em Models\Student.cs, substitua o código que você adicionou 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 os campos necessários para as propriedades de nome. O Required attribute
não é necessário para tipos de valor como DateTime, int, double e float. Os tipos de valor não podem ser atribuídos a um valor nulo, portanto, são inerentemente tratados como campos necessá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 de 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, para que você também possa 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 Cursos 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 Adicionar.
Um instrutor pode ensinar 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 Models\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; }
}
}
Compile o projeto, que salva suas alterações e verifica se você não fez nenhuma cópia e colar erros que o compilador pode capturar.
O atributo key
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 de nome de classeID
ou . 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 como 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 pode descobrir qual final da relação é a entidade de segurança 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 o final principal de uma associação entre os tipos 'ContosoUniversity.Models.OfficeAssignment' e 'ContosoUniversity.Models.Instructor'. O final principal dessa associação deve ser configurado explicitamente usando a API fluente de relação ou anotações de dados.
Posteriormente, 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 esta tabela) não é anulável.
Modificar a entidade Course
Em Models\Course.cs, substitua o código que você adicionou 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 do curso 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 onde quer que sejam necessárias. 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 do 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 navegação e chave estrangeira
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 Models\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 column
Anteriormente, você usou o atributo Column para alterar o mapeamento de nome da coluna. No código da Department
entidade, o Column
atributo está sendo usado para alterar o mapeamento de tipo de dados SQL para que a coluna seja definida usando o tipo de dinheiro 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 que você define 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 a tipos de dados SQL Server, consulte SqlClient for Entity FrameworkTypes.
Propriedades de navegação e chave estrangeira
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 definisse a
Department.InstructorID
propriedade como anulável, obteria 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 fosse anulável, 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 Models\Enrollment.cs, substitua o código que você adicionou anteriormente pelo código a seguir
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 navegação e chave estrangeira
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 Enrollment
tabela contém dados adicionais além de chaves estrangeiras para as tabelas unidas (nesse 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; criar o diagrama não faz parte do tutorial, ele está sendo usado aqui apenas 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 conteúdo (ou uma tabela de junção pura) no banco de dados, e você não precisaria criar uma classe de modelo para ele. As Instructor
entidades e Course
têm esse tipo de relação muitos para muitos e, como você pode ver, não há nenhuma 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 e Course.Instructors
.
Diagrama de relação de 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 relação um para muitos (1 para *), você pode ver aqui a linha de relação 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 à API fluente . 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 somente para mapeamento de banco de dados que não pode fazer 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. Conforme mencionado anteriormente, MinimumLength
não altera o esquema, ele aplica apenas 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. Code First pode configurar a relação muitos para muitos para você sem esse código, mas se você não chamá-lo, você 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 de lista ao construtor.
Adicionar uma migração
No PMC, insira o add-migration
comando (ainda não faça o update-database
comando):
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 stub no banco de dados para satisfazer restrições de chave estrangeira e é isso que você tem que fazer agora. O código gerado no método ComplexDataModel Up
adiciona uma chave estrangeira não anulável DepartmentID
à Course
tabela. Como já existem linhas na Course
tabela quando o código é executado, a AddColumn
operação falhará porque SQL Server não sabe qual valor colocar na coluna que não pode ser nula. Portanto, é necessário alterar o código para dar à nova coluna um valor padrão e criar um departamento stub chamado "Temp" para atuar como o departamento padrão. Como resultado, as linhas existentes Course
estarão todas relacionadas ao departamento "Temp" após a execução do Up
método. Você pode relacioná-los aos departamentos corretos no Seed
método .
Edite o < arquivo timestamp>_ComplexDataModel.cs, 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 está realçada):
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 adicionou 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 alguém ter adicionado cursos usando o aplicativo, você também gostaria de 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 timestamp>_ComplexDataModel.cs, 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 em Web.config arquivo. O exemplo a seguir mostra o nome alterado para CU_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 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 isso falhar, outra coisa que você pode tentar é inicializar novamente o banco de dados inserindo o seguinte comando no PMC:
update-database -TargetMigration:0
Abra o banco de dados no servidor Explorer como você fez anteriormente e expanda o nó Tabelas para ver que todas as tabelas foram criadas. (Se você ainda tiver o Servidor Explorer aberto desde o momento 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 tem dados nela como resultado das Instructor
entidades que você adicionou à Course.Instructors
propriedade de 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ê:
- Personalizado o modelo de dados
- Entidade student atualizada
- Criou a entidade Instructor
- Criou a entidade OfficeAssignment
- Modificou a entidade Course
- Criou a entidade Department
- Modificou a entidade Registro
- Código adicionado 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.
Comentários
https://aka.ms/ContentUserFeedback.
Em breve: Ao longo de 2024, eliminaremos os problemas do GitHub como o mecanismo de comentários para conteúdo e o substituiremos por um novo sistema de comentários. Para obter mais informações, consulteEnviar e exibir comentários de