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:

School_class_diagram

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.

Students_index_page_with_formatted_date

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.

Duas capturas de tela que mostram as diferenças no Nome e no Tipo de Dados das duas tabelas student.

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ção Department 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 a Instructor entidade e um ponto de interrogação é adicionado após a designação de int tipo para marcar a propriedade como anulável. A propriedade de navegação é nomeada Administrator , mas contém uma Instructor 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 exigissem InstructorID 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ção Course:

    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ção Student:

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

Course_many para many_relationship

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:

Instrutor Course_many para many_relationship

No entanto, uma tabela de junção é necessária no banco de dados, conforme mostrado no diagrama de banco de dados a seguir:

Instrutor Course_many a many_relationship_tables

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.

School_data_model_diagram

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 e Course , 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, como InstructorInstructorID para a InstructorID 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.)

Captura de tela que mostra a janela Explorer do Servidor. A pasta Tabelas em Contexto escolar está aberta.

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.

Table_data_in_CourseInstructor_table

Obter o código

Baixar Projeto Concluído

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.