Tutorial: Criar um modelo de dados complexo – ASP.NET MVC com EF Core

Nos tutoriais anteriores, você trabalhou com um modelo de dados simples composto por três entidades. Neste tutorial, você adicionará mais entidades e relações e personalizará o modelo de dados especificando formatação, validação e regras de mapeamento de banco de dados.

Quando terminar, as classes de entidade formarão o modelo de dados concluído mostrado na seguinte ilustração:

Entity diagram

Neste tutorial, você:

  • Personalizar o Modelo de dados
  • Fazer alterações na entidade Student
  • Criar a entidade Instructor
  • Criar a entidade OfficeAssignment
  • Modificar a entidade Course
  • Criar a entidade Department
  • Modificar a entidade Enrollment
  • Atualizar o contexto de banco de dados
  • Propagar o banco de dados com dados de teste
  • Adicionar uma migração
  • Alterar a cadeia de conexã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 seções a seguir, você criará o modelo de dados Escola completo com a adição de atributos às classes já criadas e criação de novas classes para os demais tipos de entidade 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 instrução using ao namespace System.ComponentModel.DataAnnotations e adicione os atributos DataType e DisplayFormat à propriedade EnrollmentDate, conforme mostrado no seguinte exemplo:

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 ICollection<Enrollment> Enrollments { get; set; }
    }
}

O atributo DataType é usado para especificar um tipo de dados mais específico do que o tipo intrínseco de 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 Date, Time, PhoneNumber, Currency, 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 link mailto: 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. O atributo DataType emite atributos data- HTML 5 (pronunciados “data dash”) que são reconhecidos pelos navegadores HTML 5. 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 nas CultureInfo do servidor.

O atributo DisplayFormat é usado para especificar explicitamente o formato de data:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

A configuração ApplyFormatInEditMode especifica que a formatação também deve ser aplicada quando o valor é exibido em uma caixa de texto para edição. (Talvez você não deseje ter isso em alguns campos – por exemplo, para valores de moeda, provavelmente, você não deseja exibir o símbolo de moeda na caixa de texto para edição.)

Use Você pode usar o atributo DisplayFormat por si só, mas geralmente é uma boa ideia usar o atributo DataType também. O atributo DataType transmite a semântica dos dados, ao invés de apresentar como renderizá-lo em uma tela, e oferece 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á os dados usando o formato correto de acordo com a localidade.

Para obter mais informações, confira a documentação do auxiliar de marcação <input>.

Execute o aplicativo, acesse a página Índice de Alunos e observe que as horas não são mais exibidas nas datas de registro. O mesmo será verdadeiro para qualquer exibição que usa o modelo Aluno.

Students index page showing dates without times

O atributo StringLength

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 tamanho máximo do banco de dados e fornece validação do lado do cliente e do lado do servidor para o ASP.NET Core 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 propriedades LastName e FirstMidName, conforme mostrado no seguinte exemplo:

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)]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

O atributo StringLength não impedirá que um usuário insira um espaço em branco em um nome. Use o atributo RegularExpression para aplicar restrições à entrada. Por exemplo, o seguinte código exige que o primeiro caractere esteja em maiúscula e os caracteres restantes estejam em ordem alfabética:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

O atributo MaxLength fornece uma funcionalidade semelhante ao atributo StringLength, mas não fornece a validação do lado do cliente.

Agora, o modelo de banco de dados foi alterado de uma forma que exige uma alteração no esquema de banco de dados. Você usará migrações para atualizar o esquema sem perda dos dados que podem ter sido adicionados ao banco de dados usando a interface do usuário do aplicativo.

Salve as alterações e compile o projeto. Em seguida, abra a janela Comando na pasta do projeto e insira os seguintes comandos:

dotnet ef migrations add MaxLengthOnNames
dotnet ef database update

O comando migrations add alerta que pode ocorrer perda de dados, pois a alteração torna o tamanho máximo mais curto para duas colunas. As migrações criam 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 database update executou esse código.

O carimbo de data/hora prefixado ao nome do arquivo de migrações é usado pelo Entity Framework para ordenar as migrações. Crie várias migrações antes de executar o comando de atualização de banco de dados e, em seguida, todas as migrações são aplicadas na ordem em que foram criadas.

Execute o aplicativo, selecione a guia Alunos, clique em Criar Novo e tente inserir um nome com mais de 50 caracteres. O aplicativo deve impedir isso.

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, elas receberão o mesmo nome da propriedade.

No arquivo Student.cs, adicione uma instrução using a System.ComponentModel.DataAnnotations.Schema e adicione o atributo de nome de coluna à propriedade FirstMidName, 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)]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

A adição do atributo Column altera o modelo que dá suporte ao SchoolContext e, portanto, ele não corresponde ao banco de dados.

Salve as alterações e compile o projeto. Em seguida, abra a janela Comando na pasta do projeto e insira os seguintes comandos para criar outra migração:

dotnet ef migrations add ColumnFirstName
dotnet ef database update

No Pesquisador de Objetos do SQL Server, abra o designer de tabela Aluno clicando duas vezes na tabela Aluno.

Students table in SSOX after migrations

Antes de você aplicar as duas primeiras migrações, as colunas de nome eram do tipo nvarchar(MAX). Agora elas são nvarchar(50) e o nome da coluna foi alterado de FirstMidName para FirstName.

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.

Alterações na entidade Student

Student entity

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)]
        [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 ICollection<Enrollment> Enrollments { get; set; }
    }
}

O atributo Required

O atributo Required torna as propriedades de nome campos obrigatórios. O atributo Required não é necessário para tipos que permitem valor nulo como tipos de valor (DateTime, int, double, float, etc.). Tipos que não podem ser nulos são tratados automaticamente como campos obrigatórios.

O atributo Required precisa ser usado com MinimumLength para que MinimumLength seja imposto.

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

O atributo Display

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, ela tem apenas um acessador get e nenhuma coluna FullName será gerada no banco de dados.

Criar a entidade Instructor

Instructor entity

Crie Models/Instructor.cs, substituindo o código do modelo pelo seguinte código:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

Observe que várias propriedades são as mesmas 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.

Coloque vários atributos em uma linha, de modo que você também possa escrever os atributos HireDate da seguinte maneira:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

As propriedades de navegação CourseAssignments e OfficeAssignment

As propriedades CourseAssignments e OfficeAssignment são propriedades de navegação.

Um instrutor pode ministrar qualquer quantidade de cursos e, portanto, CourseAssignments é definido como uma coleção.

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Se uma propriedade de navegação pode conter várias entidades, o tipo precisa ser uma lista na qual as entradas podem ser adicionadas, excluídas e atualizadas. Especifique ICollection<T> ou um tipo, como List<T> ou HashSet<T>. Se você especificar ICollection<T>, o EF criará uma coleção HashSet<T> por padrão.

O motivo pelo qual elas são entidades CourseAssignment é explicado abaixo na seção sobre relações muitos para muitos.

As regras de negócio do Contoso Universidade indicam que um instrutor pode ter apenas, no máximo, um escritório; portanto, a propriedade OfficeAssignment contém uma única entidade OfficeAssignment (que pode ser nulo se o escritório não está atribuído).

public OfficeAssignment OfficeAssignment { get; set; }

Criar a entidade OfficeAssignment

OfficeAssignment entity

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]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

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 o nome não segue a convenção de nomenclatura ID ou classnameID. Portanto, o atributo Key é usado para identificá-la como a chave:

[Key]
public int InstructorID { get; set; }

Você também pode usar o atributo Key se a entidade tem sua própria chave primária, mas você deseja atribuir um nome de propriedade que não seja classnameID ou ID.

Por padrão, o EF trata a chave como não gerada pelo banco de dados porque a coluna destina-se a uma relação de identificação.

A propriedade de navegação Instructor

A entidade Instructor tem uma propriedade de navegação OfficeAssignment que permite valor nulo (porque um instrutor pode não ter uma atribuição de escritório), e a entidade OfficeAssignment tem uma propriedade de navegação Instructor que não permite valor nulo (porque uma atribuição de escritório não pode existir sem um instrutor – InstructorID não permite valor nulo). Quando uma entidade Instructor tiver uma entidade OfficeAssignment relacionada, cada entidade terá uma referência à outra em sua propriedade de navegação.

Você pode colocar um atributo [Required] 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 permite valor nulo.

Modificar a entidade Course

Course entity

Em Models/Course.cs, substitua o código que você adicionou anteriormente pelo código a seguir. As alterações são realçadas.

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 Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

A entidade de curso tem uma propriedade de chave estrangeira DepartmentID que aponta para a entidade Department relacionada e ela tem uma propriedade de navegação Department.

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 chaves estrangeiras no banco de dados sempre que elas são necessárias e cria automaticamente propriedades de sombra para elas. No entanto, ter a chave estrangeira no modelo de dados pode tornar as atualizações mais simples e mais eficientes. Por exemplo, ao buscar uma entidade Course a ser editada, a entidade Department será nula se você não a carregar; portanto, ao atualizar a entidade Course, primeiro você precisará buscar a entidade Department. 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 propriedade CourseID especifica que os valores de chave primária são fornecidos pelo usuário, em vez de serem 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.

O atributo DatabaseGenerated também pode ser usado para gerar valores padrão, como no caso de colunas de banco de dados usadas para registrar a data em que uma linha foi criada ou atualizada. Para obter mais informações, consulte Propriedades geradas.

Propriedades de navegação e de 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 Department Department { get; set; }

Um curso pode ter qualquer quantidade de estudantes inscritos; portanto, a propriedade de navegação Enrollments é uma coleção:

public ICollection<Enrollment> Enrollments { get; set; }

Um curso pode ser ministrado por vários instrutores; portanto, a propriedade de navegação CourseAssignments é uma coleção (o tipo CourseAssignment é explicado posteriormente):

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Criar a entidade Department

Department entity

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 Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

O atributo Column

Anteriormente, você usou o atributo Column para alterar o mapeamento de nome de coluna. No código da entidade Department, o atributo Column está sendo usado para alterar o mapeamento de tipo de dados SQL, do modo que a coluna seja definida usando o tipo money do SQL Server no banco de dados:

[Column(TypeName="money")]
public decimal Budget { get; set; }

O mapeamento de coluna geralmente não é necessário, pois o Entity Framework escolhe o tipo de dados do SQL Server apropriado com base no tipo CLR definido para a propriedade. O tipo decimal CLR é mapeado para um tipo decimal SQL Server. Mas, nesse caso, você sabe que a coluna armazenará os valores de moeda e o tipo de dados dinheiro é mais apropriado para isso.

Propriedades de navegação e de 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 propriedade InstructorID é incluída como a chave estrangeira na entidade Instructor e um ponto de interrogação é adicionado após a designação de tipo int para marcar a propriedade como uma propriedade que permite valor nulo. A propriedade de navegação é chamada Administrator, mas contém uma entidade Instructor:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

Um departamento pode ter vários cursos e, portanto, há uma propriedade de navegação Courses:

public ICollection<Course> Courses { get; set; }

Observação

Por convenção, o Entity Framework habilita a exclusão em cascata para chaves estrangeiras que não permitem valor nulo e em relações muitos para muitos. Isso pode resultar em regras de exclusão em cascata circular, que causará uma exceção quando você tentar adicionar uma migração. Por exemplo, se você não definiu a propriedade Department.InstructorID como uma propriedade que permite valor nulo, o EF configurará uma regra de exclusão em cascata para excluir o departamento quando você excluir o instrutor, o que não é o que você deseja que aconteça. Se as regras de negócio exigissem que a propriedade InstructorID não permitisse valor nulo, você precisaria usar a seguinte instrução de API fluente para desabilitar a exclusão em cascata na relação:

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

Modificar a entidade Enrollment

Enrollment entity

Em Models/Enrollment.cs, substitua o código que você adicionou anteriormente pelo seguinte código:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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 Course Course { get; set; }
        public Student Student { get; set; }
    }
}

Propriedades de navegação e de 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 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 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. "Com conteúdo" significa que a tabela Enrollment contém dados adicionais além das chaves estrangeiras para as tabelas unidas (nesse caso, uma chave primária e uma propriedade Grade).

A ilustração a seguir mostra a aparência dessas relações em um diagrama de entidades. (Esse diagrama foi gerado usando o Entity Framework Power Tools para o EF 6.x; a criação do diagrama não faz parte do tutorial, mas está apenas sendo usada aqui como uma ilustração.)

Student-Course many to 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, ela será uma tabela de junção de muitos para muitos sem conteúdo (ou uma tabela de junção pura) no banco de dados. As entidades Instructor e Course têm esse tipo de relação muitos para muitos e a próxima etapa é criar uma classe de entidade para funcionar como uma tabela de junção sem conteúdo.

O EF Core dá suporte a tabelas de junção implícitas para relações muitos para muitos, mas esse tutorial não foi atualizado para usar uma tabela de junção implícita. Confira Relações muitos para muitos, a versão de Páginas do Razor deste tutorial que foi atualizada.

A entidade CourseAssignment

CourseAssignment entity

Crie Models/CourseAssignment.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 CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

Unir nomes de entidade

Uma tabela de junção é necessária no banco de dados para a relação muitos para muitos de Instrutor para Cursos, e ela precisa ser representada por um conjunto de entidades. É comum nomear uma entidade de junção EntityName1EntityName2, que, nesse caso, será CourseInstructor. No entanto, recomendamos que você escolha um nome que descreve a relação. Modelos de dados começam simples e aumentam, com junções não referentes a conteúdo obtendo com frequência o conteúdo mais tarde. Se você começar com um nome descritivo de entidade, não precisará alterar o nome posteriormente. O ideal é que a entidade de junção tenha seu próprio nome natural (possivelmente, uma única palavra) no domínio de negócios. Por exemplo, Manuais e Clientes podem ser vinculados por meio de Classificações. Para essa relação, CourseAssignment é uma escolha melhor que CourseInstructor.

Chave composta

Como as chaves estrangeiras não permitem valor nulo e, juntas, identificam exclusivamente cada linha da tabela, não é necessário ter uma chave primária. As propriedades InstructorID e CourseID devem funcionar como uma chave primária composta. A única maneira de identificar chaves primárias compostas no EF é usando a API fluente (isso não pode ser feito por meio de atributos). Você verá como configurar a chave primária composta na próxima seção.

A chave composta garante que, embora você possa ter várias linhas para um curso e várias linhas para um instrutor, não poderá ter várias linhas para o mesmo instrutor e curso. A entidade de junção Enrollment define sua própria chave primária e, portanto, duplicatas desse tipo são possíveis. Para evitar essas duplicatas, você pode adicionar um índice exclusivo nos campos de chave estrangeira ou configurar Enrollment com uma chave primária composta semelhante a CourseAssignment. Para obter mais informações, consulte Índices.

Atualizar o contexto de banco de dados

Adicione o seguinte código realçado ao arquivo Data/SchoolContext.cs:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

Esse código adiciona novas entidades e configura a chave primária composta da entidade CourseAssignment.

Sobre a alternativa de API fluente

O código no método OnModelCreating da classe DbContext usa a API fluente para configurar o comportamento do EF. A API é chamada "fluente" porque costuma ser usada pelo encadeamento de uma série de chamadas de método em uma única instrução, como neste exemplo da documentação do EF Core:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

Neste tutorial, você usa a API fluente somente para o mapeamento de banco de dados que não é possível 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; apenas aplica uma regra de validação do lado do cliente e do servidor.

Alguns desenvolvedores preferem usar a API fluente exclusivamente para que possam manter suas classes de entidade "limpas". Combine atributos e a API fluente se desejar. Além disso, há algumas personalizações que podem ser feitas apenas com a API fluente, mas em geral, a prática recomendada é escolher uma dessas duas abordagens e usar isso com o máximo de consistência possível. Se usar as duas, observe que sempre que houver um conflito, a API fluente substituirá atributos.

Para obter mais informações sobre atributos vs. API fluente, consulte Métodos de configuração.

Diagrama de entidade mostrando relações

A ilustração a seguir mostra o diagrama criado pelo Entity Framework Power Tools para o modelo Escola concluído.

Entity diagram

Além das linhas de relação um-para-muitos (1 para *), você pode ver a linha de relação um para zero ou um (1 para 0..1) entre as entidades Instructor e OfficeAssignment e a linha de relação zero-ou-um-para-muitos (0..1 para *) entre as entidades Instructor e Department.

Propagar o banco de dados com dados de teste

Substitua o código no arquivo Data/DbInitializer.cs pelo código a seguir para fornecer dados de semente para as novas entidades criadas.

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new 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") }
            };

            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var instructors = new 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") }
            };

            foreach (Instructor i in instructors)
            {
                context.Instructors.Add(i);
            }
            context.SaveChanges();

            var departments = new 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 }
            };

            foreach (Department d in departments)
            {
                context.Departments.Add(d);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var officeAssignments = new 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" },
            };

            foreach (OfficeAssignment o in officeAssignments)
            {
                context.OfficeAssignments.Add(o);
            }
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            foreach (CourseAssignment ci in courseInstructors)
            {
                context.CourseAssignments.Add(ci);
            }
            context.SaveChanges();

            var enrollments = new 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();
        }
    }
}

Como você viu no primeiro tutorial, a maioria do código apenas cria novos objetos de entidade e carrega dados de exemplo em propriedades, conforme necessário, para teste. Observe como as relações muitos para muitos são tratadas: o código cria relações com a criação de entidades nos conjuntos de entidades de junção Enrollments e CourseAssignment.

Adicionar uma migração

Salve as alterações e compile o projeto. Em seguida, abra a janela Comando na pasta do projeto e insira o comando migrations add (não execute ainda o comando de atualização de banco de dados):

dotnet ef migrations add ComplexDataModel

Você receberá um aviso sobre a possível perda de dados.

An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'

Se você tiver tentado executar o comando database update 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 os dados existentes, precisa inserir dados de stub no banco de dados para atender às restrições de chave estrangeira. O código gerado no método Up adiciona uma chave estrangeira DepartmentID que não permite valor nulo para a tabela Course. Se já houver linhas na tabela Curso quando o código for executado, a operação AddColumn falhará, porque o SQL Server não saberá qual valor deve ser colocado na coluna que não pode ser nulo. Para este tutorial, você executará a migração em um novo banco de dados, mas em um aplicativo de produção, você precisará fazer com que a migração manipule os dados existentes. Portanto, as instruções a seguir mostram um exemplo de como fazer isso.

Para fazer a migração funcionar com os dados existentes, você precisa alterar o código para fornecer à nova coluna um valor padrão e criar um departamento de stub chamado "Temp" para atuar como o departamento padrão. Como resultado, as linhas Curso existentes serão todas relacionadas ao departamento "Temp" após a execução do método Up.

  • Abra o arquivo {timestamp}_ComplexDataModel.cs .

  • Comente a linha de código que adiciona a coluna DepartmentID à tabela Curso.

    migrationBuilder.AlterColumn<string>(
        name: "Title",
        table: "Course",
        maxLength: 50,
        nullable: true,
        oldClrType: typeof(string),
        oldNullable: true);
                
    //migrationBuilder.AddColumn<int>(
    //    name: "DepartmentID",
    //    table: "Course",
    //    nullable: false,
    //    defaultValue: 0);
    
  • Adicione o seguinte código realçado após o código que cria a tabela Departamento:

    migrationBuilder.CreateTable(
        name: "Department",
        columns: table => new
        {
            DepartmentID = table.Column<int>(nullable: false)
                .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
            Budget = table.Column<decimal>(type: "money", nullable: false),
            InstructorID = table.Column<int>(nullable: true),
            Name = table.Column<string>(maxLength: 50, nullable: true),
            StartDate = table.Column<DateTime>(nullable: false)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_Department", x => x.DepartmentID);
            table.ForeignKey(
                name: "FK_Department_Instructor_InstructorID",
                column: x => x.InstructorID,
                principalTable: "Instructor",
                principalColumn: "ID",
                onDelete: ReferentialAction.Restrict);
        });
    
    migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
    // Default value for FK points to department created above, with
    // defaultValue changed to 1 in following AddColumn statement.
    
    migrationBuilder.AddColumn<int>(
        name: "DepartmentID",
        table: "Course",
        nullable: false,
        defaultValue: 1);
    

Em um aplicativo de produção, você escreverá código ou scripts para adicionar linhas Departamento e relacionar linhas Curso às novas linhas Departamento. Em seguida, você não precisará mais do departamento "Temp" ou do valor padrão na coluna Course.DepartmentID.

Salve as alterações e compile o projeto.

Alterar a cadeia de conexão

Agora, você tem novo código na classe DbInitializer que adiciona dados de semente para as novas entidades a um banco de dados vazio. Para fazer com que o EF crie um novo banco de dados vazio, altere o nome do banco de dados na cadeia de conexão em appsettings.json para ContosoUniversity3 ou para outro nome que você ainda não usou no computador que está sendo usado.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

Salve as alterações em appsettings.json.

Observação

Como alternativa à alteração do nome do banco de dados, você pode excluir o banco de dados. Use o SSOX (Pesquisador de Objetos do SQL Server) ou o comando database drop da CLI:

dotnet ef database drop

Atualizar o banco de dados

Depois que você tiver alterado o nome do banco de dados ou excluído o banco de dados, execute o comando database update na janela Comando para executar as migrações.

dotnet ef database update

Execute o aplicativo para fazer com que o método DbInitializer.Initialize execute e popule o novo banco de dados.

Abra o banco de dados no SSOX, como você fez anteriormente, e expanda o nó Tabelas para ver se todas as tabelas foram criadas. (Se você ainda tem o SSOX aberto do momento anterior, clique no botão Atualizar.)

Tables in SSOX

Execute o aplicativo para disparar o código inicializador que propaga o banco de dados.

Clique com o botão direito do mouse na tabela CourseAssignment e selecione Exibir Dados para verificar se existem dados nela.

CourseAssignment data in SSOX

Obter o código

Baixe ou exiba o aplicativo concluído.

Próximas etapas

Neste tutorial, você:

  • Personalizou o Modelo de dados
  • Fez alterações na entidade Student
  • Criou a entidade Instructor
  • Criou a entidade OfficeAssignment
  • Modificou a entidade Course
  • Criou a entidade Department
  • Modificou a entidade Enrollment
  • Atualizou o contexto de banco de dados
  • Propagou o banco de dados com os dados de teste
  • Adicionou uma migração
  • Alterou a cadeia de conexão
  • Atualizou o banco de dados

Vá para o próximo tutorial para aprender a acessar dados relacionados.