Parte 5, Páginas do Razor com EF Core no ASP.NET Core - Modelo de Dados
Por Tom Dykstra, Jeremy Likness e Jon P. Smith
O aplicativo Web Contoso University demonstra como criar aplicativos Web das Razor Pages usando o EF Core e o Visual Studio. Para obter informações sobre a série de tutoriais, consulte o primeiro tutorial.
Se você encontrar problemas que não possa resolver, baixe o aplicativo concluído e compare esse código com o que você criou seguindo o tutorial.
Os tutoriais anteriores trabalharam com um modelo de dados básico composto por três entidades. Neste tutorial:
- Mais entidades e relações são adicionadas.
- O modelo de dados é personalizado com a especificação das regras de formatação, validação e mapeamento de banco de dados.
O modelo de dados concluído é mostrado na seguinte ilustração:
O diagrama de banco de dados a seguir foi feito com Dataedo:
Para criar um diagrama de banco de dados com o Dataedo:
- Implantar o aplicativo no Azure
- Faça o download e instale o Dataedo no computador.
- Siga as instruções Gerar documentação para o Banco de Dados SQL do Azure em 5 minutos
No diagrama do Dataedo anterior, o CourseInstructor
é uma tabela de junção criada pelo Entity Framework. Para obter mais informações, consulte Muitos para muitos
A entidade Student
Substitua o código em Models/Student.cs
pelo seguinte código:
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 ICollection<Enrollment> Enrollments { get; set; }
}
}
O código anterior adiciona uma propriedade FullName
e adiciona os seguintes atributos às propriedades existentes:
A propriedade calculada FullName
FullName
é uma propriedade calculada que retorna um valor criado pela concatenação de duas outras propriedades. FullName
não pode ser definido, assim, ele apenas tem um acessador get. Nenhuma coluna FullName
é criada no banco de dados.
O atributo DataType
[DataType(DataType.Date)]
Para as datas de registro do aluno, todas as páginas atualmente exibem a hora do dia junto com a data, embora apenas a data seja relevante. 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 página que mostra os dados.
O atributo DataType especifica um tipo de dados mais específico do que o tipo intrínseco de banco de dados. Neste caso, apenas a data deve ser exibida, não a data e a hora. A Enumeração do Tipo de Dados fornece muitos tipos de dados, como Data, Hora, Número de Telefone, Moeda, Endereço de E-mail, etc. O atributo DataType
também pode permitir que o aplicativo forneça automaticamente recursos específicos do tipo. Por exemplo:
- O link
mailto:
é criado automaticamente paraDataType.EmailAddress
. - O seletor de data é fornecido para
DataType.Date
na maioria dos navegadores.
O atributo DataType
emite atributos HTML 5 data-
(pronunciados como data dash). Os atributos DataType
não fornecem validação.
O atributo DisplayFormat
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
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. A configuração ApplyFormatInEditMode
especifica que a formatação também deve ser aplicada à interface do usuário de edição. Alguns campos não devem usar ApplyFormatInEditMode
. Por exemplo, o símbolo de moeda geralmente não deve ser exibido em uma caixa de texto de edição.
O atributo DisplayFormat
pode ser usado por si só. Geralmente, é uma boa ideia usar o atributo DataType
com o atributo DisplayFormat
. O atributo DataType
transmite a semântica dos dados em vez de como renderizá-los em uma tela. O atributo DataType
oferece os seguintes benefícios que não estão disponíveis em DisplayFormat
:
- O navegador pode habilitar recursos do HTML5. Por exemplo, mostra um controle de calendário, o símbolo de moeda apropriado à localidade, links de email e validação de entrada do lado do cliente.
- Por padrão, o navegador renderiza os dados usando o formato correto de acordo com a localidade.
Para obter mais informações, consulte a documentação do Auxiliar de Marcação de <input>.
O atributo StringLength
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
Regras de validação de dados e mensagens de erro de validação podem ser especificadas com atributos. O atributo StringLength especifica o tamanho mínimo e máximo de caracteres permitidos em um campo de dados. O código mostrado limita os nomes a, no máximo, 50 caracteres. Um exemplo que define o comprimento mínimo da cadeia de caracteres é mostrado posteriormente.
O atributo StringLength
também fornece a validação do lado do cliente e do servidor. O valor mínimo não tem impacto sobre o esquema de banco de dados.
O atributo StringLength
não impede que um usuário insira um espaço em branco em um nome. O atributo RegularExpression pode ser usado 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]*$")]
No SSOX (Pesquisador de Objetos do SQL Server), abra o designer de tabela Aluno clicando duas vezes na tabela Aluno.
A imagem anterior mostra o esquema para a tabela Student
. Os campos de nome têm o tipo nvarchar(MAX)
. Quando uma migração é criada e aplicada posteriormente neste tutorial, os campos de nome se tornam nvarchar(50)
como resultado dos atributos de comprimento da cadeia de caracteres.
O atributo Column
[Column("FirstName")]
public string FirstMidName { get; set; }
Os atributos podem controlar como as classes e propriedades são mapeadas para o banco de dados. No modelo Student
, o atributo Column
é usado para mapear o nome da propriedade FirstMidName
para "FirstName" no banco de dados.
Quando o banco de dados é criado, os nomes de propriedade no modelo são usados para nomes de coluna (exceto quando o atributo Column
é usado). O modelo Student
usa FirstMidName
para o campo de nome porque o campo também pode conter um sobrenome.
Com o atributo [Column]
, Student.FirstMidName
no modelo de dados é mapeado para a coluna FirstName
da tabela Student
. A adição do atributo Column
altera o modelo que dá suporte ao SchoolContext
. O modelo que dá suporte ao SchoolContext
não corresponde mais ao banco de dados. Essa discrepância será resolvida adicionando uma migração posteriormente neste tutorial.
O atributo Required
[Required]
O atributo Required
torna as propriedades de nome campos obrigatórios. O atributo Required
não é necessário para tipos que não permitem valor nulo, como tipos de valor (por exemplo, DateTime
, int
e double
). 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; }
MinimumLength
e Required
permitem que o espaço em branco atenda à validação. Use o atributo RegularExpression
para obter controle total sobre a cadeia de caracteres.
O atributo Display
[Display(Name = "Last Name")]
O atributo Display
especifica que a legenda para as caixas de texto deve ser "Nome", "Sobrenome", "Nome Completo" e "Data de Inscrição". As legendas padrão não tinham espaço ao dividir as palavras, por exemplo, "Nomecompleto".
Criar uma migração
Execute o aplicativo e acesse a página Alunos. Uma exceção é gerada. O atributo [Column]
faz com que o EF Espere encontrar uma coluna chamada FirstName
, mas o nome da coluna no banco de dados ainda é FirstMidName
.
A mensagem de erro é semelhante ao exemplo a seguir:
SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:
SchoolContext
No PMC, insira os seguintes comandos para criar uma nova migração e atualizar o banco de dados:
Add-Migration ColumnFirstName Update-Database
O primeiro desses comandos gera a seguinte mensagem de aviso:
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
O aviso é gerado porque os campos de nome agora estão limitados a 50 caracteres. Se um nome no banco de dados tiver mais de 50 caracteres, o 51º caractere até o último caractere serão perdidos.
Abra a tabela Alunos no SSOX:
Antes de a migração ser aplicada, as colunas de nome eram do tipo nvarchar(MAX). As colunas de nome agora são
nvarchar(50)
. O nome da coluna foi alterado deFirstMidName
paraFirstName
.
- Execute o aplicativo e acesse a página Alunos.
- Observe que os horários não são inseridos nem exibidos juntamente com datas.
- Selecione Criar Novo e tente inserir um nome com mais de 50 caracteres.
Observação
Nas seções a seguir, a criação do aplicativo em alguns estágios gera erros do compilador. As instruções especificam quando compilar o aplicativo.
A entidade Instructor
Crie Models/Instructor.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 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<Course> Courses { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}
}
Vários atributos podem estar em uma linha. Os atributos HireDate
podem ser escritos da seguinte maneira:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Propriedades de navegação
As propriedades Courses
e OfficeAssignment
são propriedades de navegação.
Um instrutor pode ministrar qualquer quantidade de cursos e, portanto, Courses
é definido como uma coleção.
public ICollection<Course> Courses { get; set; }
Um instrutor pode ter no máximo um escritório, portanto, a propriedade OfficeAssignment
mantém uma única entidade OfficeAssignment
. OfficeAssignment
será nulo se nenhum escritório for atribuído.
public OfficeAssignment OfficeAssignment { get; set; }
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]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}
}
O atributo Key
O atributo [Key]
é usado para identificar uma propriedade como a chave primária (PK) quando o nome da propriedade é algo diferente declassnameID
ou ID
.
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. A PK OfficeAssignment
também é a FK (chave estrangeira) da entidade Instructor
. Uma relação de um para zero ou um ocorre quando uma PK em uma tabela é uma PK e uma FK em outra tabela.
EF Core não pode reconhecer automaticamente InstructorID
como a PK de OfficeAssignment
porque InstructorID
não segue a convenção de nomenclatura de ID ou classnameID. Portanto, o atributo Key
é usado para identificar InstructorID
como a PK:
[Key]
public int InstructorID { get; set; }
Por padrão, o EF Core trata a chave como não gerada pelo banco de dados porque a coluna destina-se a uma relação de identificação. Para obter mais informações, consulte Chaves do EF.
A propriedade de navegação Instructor
A propriedade de navegação Instructor.OfficeAssignment
pode ser nula porque pode não haver uma linha OfficeAssignment
para um determinado instrutor. Um instrutor pode não ter uma atribuição de escritório.
A propriedade de navegação OfficeAssignment.Instructor
sempre terá uma entidade de instrutor porque o tipo InstructorID
de chave estrangeira é int
, um tipo de valor não anulável. Uma atribuição de escritório não pode existir sem um instrutor.
Quando uma entidade Instructor
tem uma entidade OfficeAssignment
relacionada, cada entidade tem uma referência à outra em sua propriedade de navegação.
A entidade Course
Atualize Models/Course.cs
com o 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 Department Department { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<Instructor> Instructors { get; set; }
}
}
A entidade Course
tem uma propriedade de FK (chave estrangeira) DepartmentID
. DepartmentID
aponta para a entidade Department
relacionada. A entidade Course
tem uma propriedade de navegação Department
.
O EF Core não exige uma propriedade de chave estrangeira para um modelo de dados quando o modelo tem uma propriedade de navegação para uma entidade relacionada. O EF Core cria automaticamente FKs no banco de dados sempre que forem necessárias. O EF Core cria propriedades de sombra para FKs criadas automaticamente. Porém, incluir explicitamente a FK no modelo de dados pode tornar as atualizações mais simples e mais eficientes. Por exemplo, considere um modelo em que a propriedade de FK DepartmentID
não é incluída. Quando uma entidade de curso é buscada para editar:
- A propriedade
Department
seránull
se não for carregada de forma explícita. - Para atualizar a entidade de curso, a entidade
Department
primeiro deve ser buscada.
Quando a propriedade de FK DepartmentID
está incluída no modelo de dados, não é necessário buscar a entidade Department
antes de uma atualização.
O atributo DatabaseGenerated
O atributo [DatabaseGenerated(DatabaseGeneratedOption.None)]
especifica que a PK é fornecida pelo aplicativo em vez de ser gerada pelo banco de dados.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
Por padrão, o EF Core supõe que os valores de PK sejam gerados pelo banco de dados. O banco de dados gerado costuma ser a melhor abordagem. Para entidades Course
, o usuário especifica o PK. Por exemplo, um número de curso, como uma série 1000 para o departamento de matemática e uma série 2000 para o departamento em inglês.
O atributo DatabaseGenerated
também pode ser usado para gerar valores padrão. Por exemplo, o banco de dados pode gerar automaticamente um campo de data 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 FK (chave estrangeira) na entidade Course
refletem as seguintes relações:
Um curso é atribuído a um departamento; portanto, há uma FK DepartmentID
e uma propriedade de navegação Department
.
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 Instructors
é uma coleção:
public ICollection<Instructor> Instructors { get; set; }
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 Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
O atributo Column
Anteriormente, o atributo Column
foi usado para alterar o mapeamento de nome de coluna. No código da entidade Department
, o atributo Column
é usado para alterar o mapeamento de tipo de dados SQL. A coluna Budget
é definida usando o tipo de dinheiro do SQL Server no banco de dados:
[Column(TypeName="money")]
public decimal Budget { get; set; }
Em geral, o mapeamento de coluna não é necessário. O EF Core escolhe o tipo de dados do SQL Server apropriado com base no tipo de CLR da propriedade. O tipo decimal
CLR é mapeado para um tipo decimal
SQL Server. Budget
refere-se à moeda e o tipo de dados de dinheiro é mais apropriado para moeda.
Propriedades de navegação e de chave estrangeira
As propriedades de navegação e de FK refletem as seguintes relações:
- Um departamento pode ou não ter um administrador.
- Um administrador é sempre um instrutor. Portanto, a propriedade
InstructorID
está incluída como a FK da entidadeInstructor
.
A propriedade de navegação é chamada Administrator
, mas contém uma entidade Instructor
:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
O ?
no código anterior especifica que a propriedade permite valor nulo.
Um departamento pode ter vários cursos e, portanto, há uma propriedade de navegação Courses:
public ICollection<Course> Courses { get; set; }
Por convenção, o EF Core habilita a exclusão em cascata em FKs que não permitem valor nulo e em relações de muitos para muitos. Esse comportamento padrão pode resultar em regras circulares de exclusão em cascata. As regras de exclusão em cascata circular causam uma exceção quando uma migração é adicionada.
Por exemplo, se a propriedade Department.InstructorID
tiver sido definida como não anulável, o EF Core configurará uma regra de exclusão em cascata. Nesse caso, o departamento seria excluído quando o instrutor atribuído como seu administrador fosse excluído. Nesse cenário, uma regra restrita fará mais sentido. A API fluente a seguir definiria uma regra restrita e desabilitaria a exclusão em cascata.
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
A chave estrangeira de inscrição e as propriedades de navegação
Um registro se refere a um curso feito por um aluno.
Atualize Models/Enrollment.cs
com o seguinte código:
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }
public Course Course { get; set; }
public Student Student { get; set; }
}
}
As propriedades de navegação e de FK refletem as seguintes relações:
Um registro destina-se a um curso e, portanto, há uma propriedade de FK CourseID
e uma propriedade de navegação Course
:
public int CourseID { get; set; }
public Course Course { get; set; }
Um registro destina-se a um aluno e, portanto, há uma propriedade de FK 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
. A entidade Enrollment
funciona como uma tabela de junção de 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 FKs das tabelas de junção. Na entidade Enrollment
, os dados adicionais além das FKs são a PK e o Grade
.
A ilustração a seguir mostra a aparência dessas relações em um diagrama de entidades. (Esse diagrama foi gerado usando o EF Power Tools para EF 6.x. A criação do diagrama não faz parte do tutorial.)
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 FKs, CourseID
e StudentID
. Uma tabela de junção muitos para muitos sem conteúdo é às vezes chamada de PJT (uma tabela de junção pura).
As entidades Instructor
e Course
têm uma relação de muitos para muitos usando uma PJT.
Atualizar o contexto de banco de dados
Atualize Data/SchoolContext.cs
com o seguinte código:
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; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable(nameof(Course))
.HasMany(c => c.Instructors)
.WithMany(i => i.Courses);
modelBuilder.Entity<Student>().ToTable(nameof(Student));
modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
}
}
}
O código anterior adiciona as novas entidades e configura a relação de muitos para muitos entre as entidades Instructor
e Course
.
Alternativa de API fluente para atributos
O método OnModelCreating
no código anterior usa a API fluente para configurar o comportamento do EF Core. A API é chamada "fluente" porque geralmente é usada pelo encadeamento de uma série de chamadas de método em uma única instrução. O seguinte código é um exemplo da API fluente:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
Neste tutorial, a API fluente é usada apenas para o mapeamento do banco de dados que não pode ser feito com atributos. No entanto, a API fluente pode especificar a maioria das regras de formatação, validação e mapeamento que pode ser feita com atributos.
Alguns atributos como MinimumLength
não podem ser aplicados com a API fluente. MinimumLength
não altera o esquema; apenas aplica uma regra de validação de tamanho mínimo.
Alguns desenvolvedores preferem usar a API fluente exclusivamente para que possam manter suas classes de entidade limpas. Atributos e a API fluente podem ser combinados. Há algumas configurações que apenas podem ser feitas com a API fluente, por exemplo, especificando uma PK composta. Há algumas configurações que apenas podem ser feitas com atributos (MinimumLength
). A prática recomendada para uso de atributos ou da API fluente:
- Escolha uma dessas duas abordagens.
- Use a abordagem escolhida da forma mais consistente possível.
Alguns dos atributos usados neste tutorial são usados para:
- Somente validação (por exemplo,
MinimumLength
). - Apenas configuração do EF Core (por exemplo,
HasKey
). - Validação e configuração do EF Core (por exemplo,
[StringLength(50)]
).
Para obter mais informações sobre atributos vs. API fluente, consulte Métodos de configuração.
Propagar o banco de dados
Atualize o código no Data/DbInitializer.cs
:
using ContosoUniversity.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}
var alexander = new Student
{
FirstMidName = "Carson",
LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2016-09-01")
};
var alonso = new Student
{
FirstMidName = "Meredith",
LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var anand = new Student
{
FirstMidName = "Arturo",
LastName = "Anand",
EnrollmentDate = DateTime.Parse("2019-09-01")
};
var barzdukas = new Student
{
FirstMidName = "Gytis",
LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var li = new Student
{
FirstMidName = "Yan",
LastName = "Li",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var justice = new Student
{
FirstMidName = "Peggy",
LastName = "Justice",
EnrollmentDate = DateTime.Parse("2017-09-01")
};
var norman = new Student
{
FirstMidName = "Laura",
LastName = "Norman",
EnrollmentDate = DateTime.Parse("2019-09-01")
};
var olivetto = new Student
{
FirstMidName = "Nino",
LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2011-09-01")
};
var students = new Student[]
{
alexander,
alonso,
anand,
barzdukas,
li,
justice,
norman,
olivetto
};
context.AddRange(students);
var abercrombie = new Instructor
{
FirstMidName = "Kim",
LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11")
};
var fakhouri = new Instructor
{
FirstMidName = "Fadi",
LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06")
};
var harui = new Instructor
{
FirstMidName = "Roger",
LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01")
};
var kapoor = new Instructor
{
FirstMidName = "Candace",
LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15")
};
var zheng = new Instructor
{
FirstMidName = "Roger",
LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12")
};
var instructors = new Instructor[]
{
abercrombie,
fakhouri,
harui,
kapoor,
zheng
};
context.AddRange(instructors);
var officeAssignments = new OfficeAssignment[]
{
new OfficeAssignment {
Instructor = fakhouri,
Location = "Smith 17" },
new OfficeAssignment {
Instructor = harui,
Location = "Gowan 27" },
new OfficeAssignment {
Instructor = kapoor,
Location = "Thompson 304" }
};
context.AddRange(officeAssignments);
var english = new Department
{
Name = "English",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = abercrombie
};
var mathematics = new Department
{
Name = "Mathematics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = fakhouri
};
var engineering = new Department
{
Name = "Engineering",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = harui
};
var economics = new Department
{
Name = "Economics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = kapoor
};
var departments = new Department[]
{
english,
mathematics,
engineering,
economics
};
context.AddRange(departments);
var chemistry = new Course
{
CourseID = 1050,
Title = "Chemistry",
Credits = 3,
Department = engineering,
Instructors = new List<Instructor> { kapoor, harui }
};
var microeconomics = new Course
{
CourseID = 4022,
Title = "Microeconomics",
Credits = 3,
Department = economics,
Instructors = new List<Instructor> { zheng }
};
var macroeconmics = new Course
{
CourseID = 4041,
Title = "Macroeconomics",
Credits = 3,
Department = economics,
Instructors = new List<Instructor> { zheng }
};
var calculus = new Course
{
CourseID = 1045,
Title = "Calculus",
Credits = 4,
Department = mathematics,
Instructors = new List<Instructor> { fakhouri }
};
var trigonometry = new Course
{
CourseID = 3141,
Title = "Trigonometry",
Credits = 4,
Department = mathematics,
Instructors = new List<Instructor> { harui }
};
var composition = new Course
{
CourseID = 2021,
Title = "Composition",
Credits = 3,
Department = english,
Instructors = new List<Instructor> { abercrombie }
};
var literature = new Course
{
CourseID = 2042,
Title = "Literature",
Credits = 4,
Department = english,
Instructors = new List<Instructor> { abercrombie }
};
var courses = new Course[]
{
chemistry,
microeconomics,
macroeconmics,
calculus,
trigonometry,
composition,
literature
};
context.AddRange(courses);
var enrollments = new Enrollment[]
{
new Enrollment {
Student = alexander,
Course = chemistry,
Grade = Grade.A
},
new Enrollment {
Student = alexander,
Course = microeconomics,
Grade = Grade.C
},
new Enrollment {
Student = alexander,
Course = macroeconmics,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = calculus,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = trigonometry,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = composition,
Grade = Grade.B
},
new Enrollment {
Student = anand,
Course = chemistry
},
new Enrollment {
Student = anand,
Course = microeconomics,
Grade = Grade.B
},
new Enrollment {
Student = barzdukas,
Course = chemistry,
Grade = Grade.B
},
new Enrollment {
Student = li,
Course = composition,
Grade = Grade.B
},
new Enrollment {
Student = justice,
Course = literature,
Grade = Grade.B
}
};
context.AddRange(enrollments);
context.SaveChanges();
}
}
}
O código anterior fornece dados de semente para as novas entidades. A maioria desse código cria novos objetos de entidade e carrega dados de exemplo. Os dados de exemplo são usados para teste.
Aplicar a migração ou remover e recriar
Com o banco de dados existente, há duas abordagens para alterar o banco de dados:
- Remover e recriar o banco de dados. Escolha esta seção ao usar o SQLite.
- Aplicar a migração ao banco de dados existente. As instruções nesta seção funcionam apenas para SQL Server, não para o SQLite.
Qualquer opção funciona para o SQL Server. Embora o método apply-migration seja mais complexo e demorado, é a abordagem preferencial para ambientes de produção do mundo real.
Remover e recriar o banco de dados
Para forçar o EF Core a criar um novo banco de dados, remova e atualize o banco de dados:
- Exclua a pasta Migração.
- No Console do Gerenciador de Pacotes (PMC, na sigla em inglês), execute os seguintes comandos:
Drop-Database
Add-Migration InitialCreate
Update-Database
Execute o aplicativo. A execução do aplicativo executa o método DbInitializer.Initialize
. O DbInitializer.Initialize
preenche o novo banco de dados.
Abra o banco de dados no SSOX:
- Se o SSOX for aberto anteriormente, clique no botão Atualizar.
- Expanda o nó Tabelas. As tabelas criadas são exibidas.
Próximas etapas
Os próximos dois tutoriais mostram como ler e atualizar dados relacionados.
Os tutoriais anteriores trabalharam com um modelo de dados básico composto por três entidades. Neste tutorial:
- Mais entidades e relações são adicionadas.
- O modelo de dados é personalizado com a especificação das regras de formatação, validação e mapeamento de banco de dados.
O modelo de dados concluído é mostrado na seguinte ilustração:
A entidade Student
Substitua o código em Models/Student.cs
pelo seguinte código:
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 ICollection<Enrollment> Enrollments { get; set; }
}
}
O código anterior adiciona uma propriedade FullName
e adiciona os seguintes atributos às propriedades existentes:
[DataType]
[DisplayFormat]
[StringLength]
[Column]
[Required]
[Display]
A propriedade calculada FullName
FullName
é uma propriedade calculada que retorna um valor criado pela concatenação de duas outras propriedades. FullName
não pode ser definido, assim, ele apenas tem um acessador get. Nenhuma coluna FullName
é criada no banco de dados.
O atributo DataType
[DataType(DataType.Date)]
Para as datas de registro do aluno, todas as páginas atualmente exibem a hora do dia junto com a data, embora apenas a data seja relevante. 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 página que mostra os dados.
O atributo DataType especifica um tipo de dados mais específico do que o tipo intrínseco de banco de dados. Neste caso, apenas a data deve ser exibida, não a data e a hora. A Enumeração do Tipo de Dados fornece muitos tipos de dados, como Data, Hora, Número de Telefone, Moeda, Endereço de E-mail, etc. O atributo DataType
também pode permitir que o aplicativo forneça automaticamente recursos específicos do tipo. Por exemplo:
- O link
mailto:
é criado automaticamente paraDataType.EmailAddress
. - O seletor de data é fornecido para
DataType.Date
na maioria dos navegadores.
O atributo DataType
emite atributos HTML 5 data-
(pronunciados como data dash). Os atributos DataType
não fornecem validação.
O atributo DisplayFormat
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
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. A configuração ApplyFormatInEditMode
especifica que a formatação também deve ser aplicada à interface do usuário de edição. Alguns campos não devem usar ApplyFormatInEditMode
. Por exemplo, o símbolo de moeda geralmente não deve ser exibido em uma caixa de texto de edição.
O atributo DisplayFormat
pode ser usado por si só. Geralmente, é uma boa ideia usar o atributo DataType
com o atributo DisplayFormat
. O atributo DataType
transmite a semântica dos dados em vez de como renderizá-los em uma tela. O atributo DataType
oferece os seguintes benefícios que não estão disponíveis em DisplayFormat
:
- O navegador pode habilitar recursos do HTML5. Por exemplo, mostra um controle de calendário, o símbolo de moeda apropriado à localidade, links de email e validação de entrada do lado do cliente.
- Por padrão, o navegador renderiza os dados usando o formato correto de acordo com a localidade.
Para obter mais informações, consulte a documentação do Auxiliar de Marcação de <input>.
O atributo StringLength
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
Regras de validação de dados e mensagens de erro de validação podem ser especificadas com atributos. O atributo StringLength especifica o tamanho mínimo e máximo de caracteres permitidos em um campo de dados. O código mostrado limita os nomes a, no máximo, 50 caracteres. Um exemplo que define o comprimento mínimo da cadeia de caracteres é mostrado posteriormente.
O atributo StringLength
também fornece a validação do lado do cliente e do servidor. O valor mínimo não tem impacto sobre o esquema de banco de dados.
O atributo StringLength
não impede que um usuário insira um espaço em branco em um nome. O atributo RegularExpression pode ser usado 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]*$")]
No SSOX (Pesquisador de Objetos do SQL Server), abra o designer de tabela Aluno clicando duas vezes na tabela Aluno.
A imagem anterior mostra o esquema para a tabela Student
. Os campos de nome têm o tipo nvarchar(MAX)
. Quando uma migração é criada e aplicada posteriormente neste tutorial, os campos de nome se tornam nvarchar(50)
como resultado dos atributos de comprimento da cadeia de caracteres.
O atributo Column
[Column("FirstName")]
public string FirstMidName { get; set; }
Os atributos podem controlar como as classes e propriedades são mapeadas para o banco de dados. No modelo Student
, o atributo Column
é usado para mapear o nome da propriedade FirstMidName
para "FirstName" no banco de dados.
Quando o banco de dados é criado, os nomes de propriedade no modelo são usados para nomes de coluna (exceto quando o atributo Column
é usado). O modelo Student
usa FirstMidName
para o campo de nome porque o campo também pode conter um sobrenome.
Com o atributo [Column]
, Student.FirstMidName
no modelo de dados é mapeado para a coluna FirstName
da tabela Student
. A adição do atributo Column
altera o modelo que dá suporte ao SchoolContext
. O modelo que dá suporte ao SchoolContext
não corresponde mais ao banco de dados. Essa discrepância será resolvida adicionando uma migração posteriormente neste tutorial.
O atributo Required
[Required]
O atributo Required
torna as propriedades de nome campos obrigatórios. O atributo Required
não é necessário para tipos que não permitem valor nulo, como tipos de valor (por exemplo, DateTime
, int
e double
). 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; }
MinimumLength
e Required
permitem que o espaço em branco atenda à validação. Use o atributo RegularExpression
para obter controle total sobre a cadeia de caracteres.
O atributo Display
[Display(Name = "Last Name")]
O atributo Display
especifica que a legenda para as caixas de texto deve ser "Nome", "Sobrenome", "Nome Completo" e "Data de Inscrição". As legendas padrão não tinham espaço ao dividir as palavras, por exemplo, "Nomecompleto".
Criar uma migração
Execute o aplicativo e acesse a página Alunos. Uma exceção é gerada. O atributo [Column]
faz com que o EF Espere encontrar uma coluna chamada FirstName
, mas o nome da coluna no banco de dados ainda é FirstMidName
.
A mensagem de erro é semelhante ao exemplo a seguir:
SqlException: Invalid column name 'FirstName'.
No PMC, insira os seguintes comandos para criar uma nova migração e atualizar o banco de dados:
Add-Migration ColumnFirstName Update-Database
O primeiro desses comandos gera a seguinte mensagem de aviso:
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
O aviso é gerado porque os campos de nome agora estão limitados a 50 caracteres. Se um nome no banco de dados tiver mais de 50 caracteres, o 51º caractere até o último caractere serão perdidos.
Abra a tabela Alunos no SSOX:
Antes de a migração ser aplicada, as colunas de nome eram do tipo nvarchar(MAX). As colunas de nome agora são
nvarchar(50)
. O nome da coluna foi alterado deFirstMidName
paraFirstName
.
- Execute o aplicativo e acesse a página Alunos.
- Observe que os horários não são inseridos nem exibidos juntamente com datas.
- Selecione Criar Novo e tente inserir um nome com mais de 50 caracteres.
Observação
Nas seções a seguir, a criação do aplicativo em alguns estágios gera erros do compilador. As instruções especificam quando compilar o aplicativo.
A entidade Instructor
Crie Models/Instructor.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 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; }
}
}
Vários atributos podem estar em uma linha. Os atributos HireDate
podem ser escritos da seguinte maneira:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Propriedades de navegação
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; }
Um instrutor pode ter no máximo um escritório, portanto, a propriedade OfficeAssignment
mantém uma única entidade OfficeAssignment
. OfficeAssignment
será nulo se nenhum escritório for atribuído.
public OfficeAssignment OfficeAssignment { get; set; }
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]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}
}
O atributo Key
O atributo [Key]
é usado para identificar uma propriedade como a PK (chave primária) quando o nome da propriedade é algo diferente de classnameID ou ID.
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. A PK OfficeAssignment
também é a FK (chave estrangeira) da entidade Instructor
.
EF Core não pode reconhecer automaticamente InstructorID
como a PK de OfficeAssignment
porque InstructorID
não segue a convenção de nomenclatura de ID ou classnameID. Portanto, o atributo Key
é usado para identificar InstructorID
como a PK:
[Key]
public int InstructorID { get; set; }
Por padrão, o EF Core 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 propriedade de navegação Instructor.OfficeAssignment
pode ser nula porque pode não haver uma linha OfficeAssignment
para um determinado instrutor. Um instrutor pode não ter uma atribuição de escritório.
A propriedade de navegação OfficeAssignment.Instructor
sempre terá uma entidade de instrutor porque o tipo InstructorID
de chave estrangeira é int
, um tipo de valor não anulável. Uma atribuição de escritório não pode existir sem um instrutor.
Quando uma entidade Instructor
tem uma entidade OfficeAssignment
relacionada, cada entidade tem uma referência à outra em sua propriedade de navegação.
A entidade Course
Atualize Models/Course.cs
com o 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 Department Department { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<CourseAssignment> CourseAssignments { get; set; }
}
}
A entidade Course
tem uma propriedade de FK (chave estrangeira) DepartmentID
. DepartmentID
aponta para a entidade Department
relacionada. A entidade Course
tem uma propriedade de navegação Department
.
O EF Core não exige uma propriedade de chave estrangeira para um modelo de dados quando o modelo tem uma propriedade de navegação para uma entidade relacionada. O EF Core cria automaticamente FKs no banco de dados sempre que forem necessárias. O EF Core cria propriedades de sombra para FKs criadas automaticamente. Porém, incluir explicitamente a FK no modelo de dados pode tornar as atualizações mais simples e mais eficientes. Por exemplo, considere um modelo em que a propriedade de FK DepartmentID
não é incluída. Quando uma entidade de curso é buscada para editar:
- A propriedade
Department
será nula se não for carregada de forma explícita. - Para atualizar a entidade de curso, a entidade
Department
primeiro deve ser buscada.
Quando a propriedade de FK DepartmentID
está incluída no modelo de dados, não é necessário buscar a entidade Department
antes de uma atualização.
O atributo DatabaseGenerated
O atributo [DatabaseGenerated(DatabaseGeneratedOption.None)]
especifica que a PK é fornecida pelo aplicativo em vez de ser gerada pelo banco de dados.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
Por padrão, o EF Core supõe que os valores de PK sejam gerados pelo banco de dados. O banco de dados gerado costuma ser a melhor abordagem. Para entidades Course
, o usuário especifica o PK. Por exemplo, um número de curso, como uma série 1000 para o departamento de matemática e uma série 2000 para o departamento em inglês.
O atributo DatabaseGenerated
também pode ser usado para gerar valores padrão. Por exemplo, o banco de dados pode gerar automaticamente um campo de data 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 FK (chave estrangeira) na entidade Course
refletem as seguintes relações:
Um curso é atribuído a um departamento; portanto, há uma FK DepartmentID
e uma propriedade de navegação Department
.
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:
public ICollection<CourseAssignment> CourseAssignments { get; set; }
CourseAssignment
é explicado posteriormente.
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 Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
O atributo Column
Anteriormente, o atributo Column
foi usado para alterar o mapeamento de nome de coluna. No código da entidade Department
, o atributo Column
é usado para alterar o mapeamento de tipo de dados SQL. A coluna Budget
é definida usando o tipo de dinheiro do SQL Server no banco de dados:
[Column(TypeName="money")]
public decimal Budget { get; set; }
Em geral, o mapeamento de coluna não é necessário. O EF Core escolhe o tipo de dados do SQL Server apropriado com base no tipo de CLR da propriedade. O tipo decimal
CLR é mapeado para um tipo decimal
SQL Server. Budget
refere-se à moeda e o tipo de dados de dinheiro é mais apropriado para moeda.
Propriedades de navegação e de chave estrangeira
As propriedades de navegação e de FK refletem as seguintes relações:
- Um departamento pode ou não ter um administrador.
- Um administrador é sempre um instrutor. Portanto, a propriedade
InstructorID
está incluída como a FK da entidadeInstructor
.
A propriedade de navegação é chamada Administrator
, mas contém uma entidade Instructor
:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
O ponto de interrogação (?) no código anterior especifica que a propriedade permite valor nulo.
Um departamento pode ter vários cursos e, portanto, há uma propriedade de navegação Courses:
public ICollection<Course> Courses { get; set; }
Por convenção, o EF Core habilita a exclusão em cascata em FKs que não permitem valor nulo e em relações de muitos para muitos. Esse comportamento padrão pode resultar em regras circulares de exclusão em cascata. As regras de exclusão em cascata circular causam uma exceção quando uma migração é adicionada.
Por exemplo, se a propriedade Department.InstructorID
tiver sido definida como não anulável, o EF Core configurará uma regra de exclusão em cascata. Nesse caso, o departamento seria excluído quando o instrutor atribuído como seu administrador fosse excluído. Nesse cenário, uma regra restrita fará mais sentido. A API fluente a seguir definiria uma regra restrita e desabilitaria a exclusão em cascata.
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
A entidade Enrollment
Um registro se refere a um curso feito por um aluno.
Atualize Models/Enrollment.cs
com o 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 FK refletem as seguintes relações:
Um registro destina-se a um curso e, portanto, há uma propriedade de FK CourseID
e uma propriedade de navegação Course
:
public int CourseID { get; set; }
public Course Course { get; set; }
Um registro destina-se a um aluno e, portanto, há uma propriedade de FK 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
. A 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 FKs das tabelas unidas (nesse caso, a FK e Grade
).
A ilustração a seguir mostra a aparência dessas relações em um diagrama de entidades. (Esse diagrama foi gerado usando o EF Power Tools para EF 6.x. A criação do diagrama não faz parte do tutorial.)
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 FKs (CourseID
e StudentID
). Uma tabela de junção muitos para muitos sem conteúdo é às vezes chamada de PJT (uma tabela de junção pura).
As entidades Instructor
e Course
têm uma relação muitos para muitos usando uma tabela de junção pura.
Observação: o EF 6.x é compatível com tabelas de junção implícita para relações de muitos para muitos, mas o EF Core não é. Para obter mais informações, consulte Relações de muitos para muitos no EF Core 2.0.
A entidade CourseAssignment
Crie Models/CourseAssignment.cs
com o seguinte código:
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; }
}
}
O relacionamento de muitos para muitos do instrutor para cursos requer uma tabela de junção e a entidade para essa tabela de junção é CourseAssignment.
É comum nomear uma entidade de junção EntityName1EntityName2
. Por exemplo, a tabela de junção Instrutor para Cursos com esse padrão seria CourseInstructor
. No entanto, recomendamos que você use um nome que descreve a relação.
Modelos de dados começam simples e aumentam. As PJTs (tabelas de junção sem payload) costumam evoluir para incluir a payload. Começando com um nome descritivo de entidade, o nome não precisa ser alterado quando a tabela de junção é alterada. 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 com uma entidade de junção chamada Ratings. Para a relação muitos para muitos de Instrutor para Cursos, CourseAssignment
é preferível a CourseInstructor
.
Chave composta
As duas FKs em CourseAssignment
(InstructorID
e CourseID
) juntas identificam exclusivamente cada linha da tabela CourseAssignment
. CourseAssignment
não exige um PK dedicado. As propriedades InstructorID
e CourseID
funcionam como uma PK composta. A única maneira de especificar PKs compostas no EF Core é com a API fluente. A próxima seção mostra como configurar a PK composta.
A chave composta garante que:
- Várias linhas são permitidas para um curso.
- Várias linhas são permitidas para um instrutor.
- Não sejam permitidas várias linhas para o mesmo instrutor e curso.
A entidade de junção Enrollment
define sua própria PK e, portanto, duplicatas desse tipo são possíveis. Para impedir duplicatas como essas:
- Adicione um índice exclusivo nos campos de FK ou
- Configure
Enrollment
com uma chave primária composta semelhante aCourseAssignment
. Para obter mais informações, consulte Índices.
Atualizar o contexto de banco de dados
Atualize Data/SchoolContext.cs
com o seguinte código:
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 });
}
}
}
O código anterior adiciona novas entidades e configura a PK composta da entidade CourseAssignment
.
Alternativa de API fluente para atributos
O método OnModelCreating
no código anterior usa a API fluente para configurar o comportamento do EF Core. A API é chamada "fluente" porque geralmente é usada pelo encadeamento de uma série de chamadas de método em uma única instrução. O seguinte código é um exemplo da API fluente:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
Neste tutorial, a API fluente é usada apenas para o mapeamento do banco de dados que não pode ser feito com atributos. No entanto, a API fluente pode especificar a maioria das regras de formatação, validação e mapeamento que pode ser feita com atributos.
Alguns atributos como MinimumLength
não podem ser aplicados com a API fluente. MinimumLength
não altera o esquema; apenas aplica uma regra de validação de tamanho mínimo.
Alguns desenvolvedores preferem usar a API fluente exclusivamente para que possam manter suas classes de entidade "limpas". Atributos e a API fluente podem ser misturados. Há algumas configurações que apenas podem ser feitas com a API fluente (especificando uma PK composta). Há algumas configurações que apenas podem ser feitas com atributos (MinimumLength
). A prática recomendada para uso de atributos ou da API fluente:
- Escolha uma dessas duas abordagens.
- Use a abordagem escolhida da forma mais consistente possível.
Alguns dos atributos usados neste tutorial são usados para:
- Somente validação (por exemplo,
MinimumLength
). - Apenas configuração do EF Core (por exemplo,
HasKey
). - Validação e configuração do EF Core (por exemplo,
[StringLength(50)]
).
Para obter mais informações sobre atributos vs. API fluente, consulte Métodos de configuração.
Diagrama de entidade
A ilustração a seguir mostra o diagrama criado pelo EF Power Tools para o modelo Escola concluído.
O diagrama acima mostra:
- Várias linhas de relação um-para-muitos (1 para *).
- A linha de relação um para zero ou um (1 para 0..1) entre as entidades
Instructor
eOfficeAssignment
. - A linha de relação zero-ou-um-para-muitos (0..1 para *) entre as entidades
Instructor
eDepartment
.
Propagar o banco de dados
Atualize o código no Data/DbInitializer.cs
:
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("2016-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2018-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2019-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2018-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2018-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice",
EnrollmentDate = DateTime.Parse("2017-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman",
EnrollmentDate = DateTime.Parse("2019-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2011-09-01") }
};
context.Students.AddRange(students);
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") }
};
context.Instructors.AddRange(instructors);
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 }
};
context.Departments.AddRange(departments);
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
},
};
context.Courses.AddRange(courses);
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" },
};
context.OfficeAssignments.AddRange(officeAssignments);
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
},
};
context.CourseAssignments.AddRange(courseInstructors);
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();
}
}
}
O código anterior fornece dados de semente para as novas entidades. A maioria desse código cria novos objetos de entidade e carrega dados de exemplo. Os dados de exemplo são usados para teste. Consulte Enrollments
e CourseAssignments
para obter exemplos de como tabelas de junção muitos para muitos podem ser propagadas.
Adicionar uma migração
Compile o projeto.
No PMC, execute o seguinte comando.
Add-Migration ComplexDataModel
O comando anterior exibe 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.
To undo this action, use 'ef migrations remove'
Se o comando database update
é executado, o seguinte erro é produzido:
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.
Na próxima seção, você verá o que fazer sobre esse erro.
Aplicar a migração ou remover e recriar
Agora que você tem um banco de dados existente, precisa pensar sobre como aplicar as alterações a ele. Este tutorial mostra duas alternativas:
- Remover e recriar o banco de dados. Escolha esta seção se você estiver usando o SQLite.
- Aplicar a migração ao banco de dados existente. As instruções nesta seção funcionam apenas para SQL Server, não para o SQLite.
Qualquer opção funciona para o SQL Server. Embora o método apply-migration seja mais complexo e demorado, é a abordagem preferencial para ambientes de produção do mundo real.
Remover e recriar o banco de dados
Ignore esta seção se você estiver usando SQL Server e desejar fazer a abordagem de migração de aplicação na seção a seguir.
Para forçar o EF Core a criar um novo banco de dados, remova e atualize o banco de dados:
No PMC (Console do Gerenciador de Pacotes), execute o seguinte comando:
Drop-Database
Exclua a pasta Migrations e execute o seguinte comando:
Add-Migration InitialCreate Update-Database
Execute o aplicativo. A execução do aplicativo executa o método DbInitializer.Initialize
. O DbInitializer.Initialize
preenche o novo banco de dados.
Abra o banco de dados no SSOX:
Se o SSOX for aberto anteriormente, clique no botão Atualizar.
Expanda o nó Tabelas. As tabelas criadas são exibidas.
Examine a tabela CourseAssignment:
- Clique com o botão direito do mouse na tabela CourseAssignment e selecione Exibir Dados.
- Verifique se a tabela CourseAssignment contém dados.
Aplicar a migração
Esta seção é opcional. Estas etapas só funcionarão para o LocalDB do SQL Server e apenas se você tiver ignorado a seção Remover e recriar o banco de dados anterior.
Quando as migrações são executadas com os dados existentes, pode haver restrições de FK que não são atendidas com os dados existentes. Com os dados de produção, é necessário executar etapas para migrar os dados existentes. Esta seção fornece um exemplo de correção de violações de restrição de FK. Não faça essas alterações de código sem um backup. Não faça essas alterações de código se você tiver concluído a seção Remover e recriar o banco de dados anterior.
O arquivo {timestamp}_ComplexDataModel.cs
contém o seguinte código:
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
type: "int",
nullable: false,
defaultValue: 0);
O código anterior adiciona uma FK DepartmentID
que não permite valor nulo à tabela Course
. O banco de dados do tutorial anterior contém linhas em Course
e, portanto, essa tabela não pode ser atualizada por migrações.
Para fazer a migração ComplexDataModel
funcionar com os dados existentes:
- Altere o código para dar à nova coluna (
DepartmentID
) um valor padrão. - Crie um departamento fictício chamado "Temp" para atuar como o departamento padrão.
Corrigir as restrições de chave estrangeira
Na classe de migração ComplexDataModel
, atualize o método Up
:
- Abra o arquivo
{timestamp}_ComplexDataModel.cs
. - Comente a linha de código que adiciona a coluna
DepartmentID
à tabelaCourse
.
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 código realçado a seguir. O novo código é inserido após o bloco .CreateTable( name: "Department"
:
migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(type: "int", nullable: true),
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(type: "datetime2", 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);
Com as alterações anteriores, as linhas Course
existentes estarão relacionadas ao departamento "Temp" após a execução do método ComplexDataModel.Up
.
A maneira de lidar com a situação mostrada aqui é simplificada para este tutorial. Um aplicativo de produção:
- Inclui código ou scripts para adicionar linhas
Department
e linhasCourse
relacionadas às novas linhasDepartment
. - Não usa o departamento "Temp" nem o valor padrão para
Course.DepartmentID
.
No PMC (Console do Gerenciador de Pacotes), execute o seguinte comando:
Update-Database
Como o método DbInitializer.Initialize
foi projetado para funcionar apenas com um banco de dados vazio, use SSOX para excluir todas as linhas nas tabelas Student e Course. (A exclusão em cascata cuidará da tabela de Registro.)
Execute o aplicativo. A execução do aplicativo executa o método DbInitializer.Initialize
. O DbInitializer.Initialize
preenche o novo banco de dados.
Próximas etapas
Os próximos dois tutoriais mostram como ler e atualizar dados relacionados.
Os tutoriais anteriores trabalharam com um modelo de dados básico composto por três entidades. Neste tutorial:
- Mais entidades e relações são adicionadas.
- O modelo de dados é personalizado com a especificação das regras de formatação, validação e mapeamento de banco de dados.
As classes de entidade para o modelo de dados concluído são mostradas na seguinte ilustração:
Caso tenha problemas que não consiga resolver, baixe o aplicativo concluído.
Personalizar o modelo de dados com atributos
Nesta seção, o modelo de dados é personalizado com atributos.
O atributo DataType
As páginas de alunos atualmente exibem a hora da data de registro. Normalmente, os campos de data mostram apenas a data e não a hora.
Atualize Models/Student.cs
com o seguinte código realçado:
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 especifica um tipo de dados mais específico do que o tipo intrínseco de banco de dados. Neste caso, apenas a data deve ser exibida, não a data e a hora. A Enumeração do Tipo de Dados fornece muitos tipos de dados, como Data, Hora, Número de Telefone, Moeda, Endereço de E-mail, etc. O atributo DataType
também pode permitir que o aplicativo forneça automaticamente recursos específicos do tipo. Por exemplo:
- O link
mailto:
é criado automaticamente paraDataType.EmailAddress
. - O seletor de data é fornecido para
DataType.Date
na maioria dos navegadores.
O atributo DataType
emite atributos data-
HTML 5 (pronunciados “data dash”) que são consumidos pelos navegadores HTML 5. Os atributos DataType
não fornecem 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 à interface do usuário de edição. Alguns campos não devem usar ApplyFormatInEditMode
. Por exemplo, o símbolo de moeda geralmente não deve ser exibido em uma caixa de texto de edição.
O atributo DisplayFormat
pode ser usado por si só. Geralmente, é uma boa ideia usar o atributo DataType
com o atributo DisplayFormat
. O atributo DataType
transmite a semântica dos dados em vez de como renderizá-los em uma tela. O atributo DataType
oferece os seguintes benefícios que não estão disponíveis em DisplayFormat
:
- O navegador pode habilitar recursos do HTML5. Por exemplo, mostra um controle de calendário, o símbolo de moeda apropriado à localidade, links de email, validação de entrada do lado do cliente, etc.
- Por padrão, o navegador renderiza os dados usando o formato correto de acordo com a localidade.
Para obter mais informações, consulte a documentação do Auxiliar de Marcação de <input>.
Execute o aplicativo. Navegue para a página Índice de Alunos. As horas não são mais exibidas. Cada exibição que usa o modelo Student
exibe a data sem a hora.
O atributo StringLength
Regras de validação de dados e mensagens de erro de validação podem ser especificadas com atributos. O atributo StringLength especifica o tamanho mínimo e máximo de caracteres permitidos em um campo de dados. O atributo StringLength
também fornece a validação do lado do cliente e do servidor. O valor mínimo não tem impacto sobre o esquema de banco de dados.
Atualize o modelo Student
com o seguinte código:
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 ICollection<Enrollment> Enrollments { get; set; }
}
}
O código anterior limita os nomes a, no máximo, 50 caracteres. O atributo StringLength
não impede que um usuário insira um espaço em branco em um nome. O atributo RegularExpression é usado 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]*$")]
Executar o aplicativo:
- Navegue para a página Alunos.
- Selecione Criar Novo e insira um nome com mais de 50 caracteres.
- Selecione Criar e a validação do lado do cliente mostrará uma mensagem de erro.
No SSOX (Pesquisador de Objetos do SQL Server), abra o designer de tabela Aluno clicando duas vezes na tabela Aluno.
A imagem anterior mostra o esquema para a tabela Student
. Os campos de nome têm o tipo nvarchar(MAX)
porque as migrações não foram executadas no BD. Quando as migrações forem executadas mais adiante neste tutorial, os campos de nome se tornarão nvarchar(50)
.
O atributo Column
Os atributos podem controlar como as classes e propriedades são mapeadas para o banco de dados. Nesta seção, o atributo Column
é usado para mapear o nome da propriedade FirstMidName
como "FirstName" no BD.
Quando o BD é criado, os nomes de propriedade no modelo são usados para nomes de coluna (exceto quando o atributo Column
é usado).
O modelo Student
usa FirstMidName
para o campo de nome porque o campo também pode conter um sobrenome.
Atualize o arquivo Student.cs
com o 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 ICollection<Enrollment> Enrollments { get; set; }
}
}
Com a alteração anterior, Student.FirstMidName
no aplicativo é mapeado para a coluna FirstName
da tabela Student
.
A adição do atributo Column
altera o modelo que dá suporte ao SchoolContext
. O modelo que dá suporte ao SchoolContext
não corresponde mais ao banco de dados. Se o aplicativo for executado antes da aplicação das migrações, a seguinte exceção será gerada:
SqlException: Invalid column name 'FirstName'.
Para atualizar o BD:
- Compile o projeto.
- Abra uma janela Comando na pasta do projeto. Insira os seguintes comandos para criar uma nova migração e atualizar o BD:
Add-Migration ColumnFirstName
Update-Database
O comando migrations add ColumnFirstName
gera a seguinte mensagem de aviso:
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
O aviso é gerado porque os campos de nome agora estão limitados a 50 caracteres. Se um nome no BD tiver mais de 50 caracteres, o 51º caractere até o último caractere serão perdidos.
- Testar o aplicativo.
Abra a tabela Alunos no SSOX:
Antes de a migração ser aplicada, as colunas de nome eram do tipo nvarchar(MAX). As colunas de nome agora são nvarchar(50)
. O nome da coluna foi alterado de FirstMidName
para FirstName
.
Observação
Na seção a seguir, a criação do aplicativo em alguns estágios gera erros do compilador. As instruções especificam quando compilar o aplicativo.
Atualização da entidade Student
Atualize Models/Student.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 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 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 não permitem valor nulo, como tipos de valor (DateTime
, int
, double
, etc.). Tipos que não podem ser nulos são tratados automaticamente como campos obrigatórios.
O atributo Required
pode ser substituído por um parâmetro de tamanho mínimo no atributo StringLength
:
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
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 Inscrição". As legendas padrão não tinham espaço ao dividir as palavras, por exemplo, "Nomecompleto".
A propriedade calculada FullName
FullName
é uma propriedade calculada que retorna um valor criado pela concatenação de duas outras propriedades. FullName
não pode ser definido; ele apenas tem um acessador get. Nenhuma coluna FullName
é criada no banco de dados.
Criar a entidade Instructor
Crie Models/Instructor.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 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; }
}
}
Vários atributos podem estar em uma linha. Os atributos HireDate
podem ser escritos 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 armazenar várias entidades:
- Ele deve ser um tipo de lista no qual as entradas possam ser adicionadas, excluídas e atualizadas.
Os tipos de propriedade de navegação incluem:
ICollection<T>
List<T>
HashSet<T>
Se ICollection<T>
for especificado, o EF Core criará uma coleção HashSet<T>
por padrão.
A entidade CourseAssignment
é explicada na seção sobre relações muitos para muitos.
Regras de negócio do Contoso University indicam que um instrutor pode ter, no máximo, um escritório. A propriedade OfficeAssignment
contém uma única entidade OfficeAssignment
. OfficeAssignment
será nulo se nenhum escritório for atribuído.
public 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]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}
}
O atributo Key
O atributo [Key]
é usado para identificar uma propriedade como a PK (chave primária) quando o nome da propriedade é algo diferente de classnameID ou ID.
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. A PK OfficeAssignment
também é a FK (chave estrangeira) da entidade Instructor
. O EF Core não pode reconhecer automaticamente o InstructorID
como a PK do OfficeAssignment
porque:
InstructorID
não segue a convenção de nomenclatura de ID nem de classnameID.
Portanto, o atributo Key
é usado para identificar InstructorID
como a PK:
[Key]
public int InstructorID { get; set; }
Por padrão, o EF Core 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 propriedade de navegação OfficeAssignment
da entidade Instructor
permite valor nulo porque:
- Tipos de referência (como classes que permitem valor nulo).
- Um instrutor pode não ter uma atribuição de escritório.
A entidade OfficeAssignment
tem uma propriedade de navegação Instructor
que não permite valor nulo porque:
InstructorID
não permite valor nulo.- Uma atribuição de escritório não pode existir sem um instrutor.
Quando uma entidade Instructor
tem uma entidade OfficeAssignment
relacionada, cada entidade tem uma referência à outra em sua propriedade de navegação.
O atributo [Required]
pode ser aplicado à propriedade de navegação Instructor
:
[Required]
public Instructor Instructor { get; set; }
O código anterior especifica que deve haver um instrutor relacionado. O código anterior é desnecessário porque a chave estrangeira InstructorID
(que também é a PK) não permite valor nulo.
Modificar a entidade Course
Atualize Models/Course.cs
com o 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 Department Department { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<CourseAssignment> CourseAssignments { get; set; }
}
}
A entidade Course
tem uma propriedade de FK (chave estrangeira) DepartmentID
. DepartmentID
aponta para a entidade Department
relacionada. A entidade Course
tem uma propriedade de navegação Department
.
O EF Core não exige uma propriedade de FK para um modelo de dados quando o modelo tem uma propriedade de navegação para uma entidade relacionada.
O EF Core cria automaticamente FKs no banco de dados sempre que forem necessárias. O EF Core cria propriedades de sombra para FKs criadas automaticamente. Ter a FK no modelo de dados pode tornar as atualizações mais simples e mais eficientes. Por exemplo, considere um modelo em que a propriedade de FK DepartmentID
não é incluída. Quando uma entidade de curso é buscada para editar:
- A entidade
Department
será nula se não for carregada de forma explícita. - Para atualizar a entidade de curso, a entidade
Department
primeiro deve ser buscada.
Quando a propriedade de FK DepartmentID
está incluída no modelo de dados, não é necessário buscar a entidade Department
antes de uma atualização.
O atributo DatabaseGenerated
O atributo [DatabaseGenerated(DatabaseGeneratedOption.None)]
especifica que a PK é fornecida pelo aplicativo em vez de ser gerada pelo banco de dados.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
Por padrão, o EF Core supõe que os valores de PK sejam gerados pelo BD. Os valores de PK gerados pelo BD geralmente são a melhor abordagem. Para entidades Course
, o usuário especifica o PK. Por exemplo, um número de curso, como uma série 1000 para o departamento de matemática e uma série 2000 para o departamento em inglês.
O atributo DatabaseGenerated
também pode ser usado para gerar valores padrão. Por exemplo, o BD pode gerar automaticamente um campo de data 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 FK (chave estrangeira) na entidade Course
refletem as seguintes relações:
Um curso é atribuído a um departamento; portanto, há uma FK DepartmentID
e uma propriedade de navegação Department
.
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:
public ICollection<CourseAssignment> CourseAssignments { get; set; }
CourseAssignment
é explicado posteriormente.
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 Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
O atributo Column
Anteriormente, o atributo Column
foi usado para alterar o mapeamento de nome de coluna. No código da entidade Department
, o atributo Column
é usado para alterar o mapeamento de tipo de dados SQL. A coluna Budget
é definida usando o tipo de dinheiro do SQL Server no BD:
[Column(TypeName="money")]
public decimal Budget { get; set; }
Em geral, o mapeamento de coluna não é necessário. Em geral, o EF Core escolhe o tipo de dados do SQL Server apropriado com base no tipo CLR da propriedade. O tipo decimal
CLR é mapeado para um tipo decimal
SQL Server. Budget
refere-se à moeda e o tipo de dados de dinheiro é mais apropriado para moeda.
Propriedades de navegação e de chave estrangeira
As propriedades de navegação e de FK refletem as seguintes relações:
- Um departamento pode ou não ter um administrador.
- Um administrador é sempre um instrutor. Portanto, a propriedade
InstructorID
está incluída como a FK da entidadeInstructor
.
A propriedade de navegação é chamada Administrator
, mas contém uma entidade Instructor
:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
O ponto de interrogação (?) no código anterior especifica que a propriedade permite valor nulo.
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 EF Core habilita a exclusão em cascata em FKs que não permitem valor nulo e em relações de muitos para muitos. A exclusão em cascata pode resultar em regras de exclusão em cascata circular. As regras de exclusão em cascata circular causam uma exceção quando uma migração é adicionada.
Por exemplo, se a propriedade Department.InstructorID
tiver sido definida como não anulável:
O EF Core configura uma regra de exclusão em cascata para excluir o departamento quando o instrutor é excluído.
Excluir o departamento quando o instrutor é excluído não é o comportamento desejado.
A seguinte API fluente definiria uma regra de restrição em vez de em cascata.
modelBuilder.Entity<Department>() .HasOne(d => d.Administrator) .WithMany() .OnDelete(DeleteBehavior.Restrict)
O código anterior desabilita a exclusão em cascata na relação departamento-instrutor.
Atualizar a entidade Enrollment
Um registro se refere a um curso feito por um aluno.
Atualize Models/Enrollment.cs
com o 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 FK refletem as seguintes relações:
Um registro destina-se a um curso e, portanto, há uma propriedade de FK CourseID
e uma propriedade de navegação Course
:
public int CourseID { get; set; }
public Course Course { get; set; }
Um registro destina-se a um aluno e, portanto, há uma propriedade de FK 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
. A 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 FKs das tabelas unidas (nesse caso, a FK e Grade
).
A ilustração a seguir mostra a aparência dessas relações em um diagrama de entidades. (Esse diagrama foi gerado usando o EF Power Tools para EF 6.x. A criação do diagrama não faz parte do tutorial.)
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 FKs (CourseID
e StudentID
). Uma tabela de junção muitos para muitos sem conteúdo é às vezes chamada de PJT (uma tabela de junção pura).
As entidades Instructor
e Course
têm uma relação muitos para muitos usando uma tabela de junção pura.
Observação: o EF 6.x é compatível com tabelas de junção implícita para relações de muitos para muitos, mas o EF Core não é. Para obter mais informações, consulte Relações de muitos para muitos no EF Core 2.0.
A entidade CourseAssignment
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; }
}
}
Instrutor para Cursos
A relação muitos para muitos de Instrutor para Cursos:
- Exige que uma tabela de junção seja representada por um conjunto de entidades.
- É uma tabela de junção pura (tabela sem conteúdo).
É comum nomear uma entidade de junção EntityName1EntityName2
. Por exemplo, a tabela de junção Instrutor para Cursos com esse padrão é CourseInstructor
. No entanto, recomendamos que você use um nome que descreve a relação.
Modelos de dados começam simples e aumentam. PJTs (junções sem conteúdo) evoluem com frequência para incluir o conteúdo. Começando com um nome descritivo de entidade, o nome não precisa ser alterado quando a tabela de junção é alterada. 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 com uma entidade de junção chamada Ratings. Para a relação muitos para muitos de Instrutor para Cursos, CourseAssignment
é preferível a CourseInstructor
.
Chave composta
As FKs não permitem valor nulo. As duas FKs em CourseAssignment
(InstructorID
e CourseID
) juntas identificam exclusivamente cada linha da tabela CourseAssignment
. CourseAssignment
não exige um PK dedicado. As propriedades InstructorID
e CourseID
funcionam como uma PK composta. A única maneira de especificar PKs compostas no EF Core é com a API fluente. A próxima seção mostra como configurar a PK composta.
A chave composta garante:
- Várias linhas são permitidas para um curso.
- Várias linhas são permitidas para um instrutor.
- Não é permitido ter várias linhas para o mesmo instrutor e curso.
A entidade de junção Enrollment
define sua própria PK e, portanto, duplicatas desse tipo são possíveis. Para impedir duplicatas como essas:
- Adicione um índice exclusivo nos campos de FK ou
- Configure
Enrollment
com uma chave primária composta semelhante aCourseAssignment
. Para obter mais informações, consulte Índices.
Atualizar o contexto de BD
Adicione o código realçado a seguir a Data/SchoolContext.cs
:
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Models
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollment { get; set; }
public DbSet<Student> Student { 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 });
}
}
}
O código anterior adiciona novas entidades e configura a PK composta da entidade CourseAssignment
.
Alternativa de API fluente para atributos
O método OnModelCreating
no código anterior usa a API fluente para configurar o comportamento do EF Core. A API é chamada "fluente" porque geralmente é usada pelo encadeamento de uma série de chamadas de método em uma única instrução. O seguinte código é um exemplo da API fluente:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
Neste tutorial, a API fluente é usada apenas para o mapeamento do BD que não pode ser feito com atributos. No entanto, a API fluente pode especificar a maioria das regras de formatação, validação e mapeamento que pode ser feita com atributos.
Alguns atributos como MinimumLength
não podem ser aplicados com a API fluente. MinimumLength
não altera o esquema; apenas aplica uma regra de validação de tamanho mínimo.
Alguns desenvolvedores preferem usar a API fluente exclusivamente para que possam manter suas classes de entidade "limpas". Atributos e a API fluente podem ser misturados. Há algumas configurações que apenas podem ser feitas com a API fluente (especificando uma PK composta). Há algumas configurações que apenas podem ser feitas com atributos (MinimumLength
). A prática recomendada para uso de atributos ou da API fluente:
- Escolha uma dessas duas abordagens.
- Use a abordagem escolhida da forma mais consistente possível.
Alguns dos atributos usados neste tutorial são usados para:
- Somente validação (por exemplo,
MinimumLength
). - Apenas configuração do EF Core (por exemplo,
HasKey
). - Validação e configuração do EF Core (por exemplo,
[StringLength(50)]
).
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 EF Power Tools para o modelo Escola concluído.
O diagrama acima mostra:
- Várias linhas de relação um-para-muitos (1 para *).
- A linha de relação um para zero ou um (1 para 0..1) entre as entidades
Instructor
eOfficeAssignment
. - A linha de relação zero-ou-um-para-muitos (0..1 para *) entre as entidades
Instructor
eDepartment
.
Propagar o BD com os dados de teste
Atualize o código no Data/DbInitializer.cs
:
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.Student.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.Student.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.Enrollment.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollment.Add(e);
}
}
context.SaveChanges();
}
}
}
O código anterior fornece dados de semente para as novas entidades. A maioria desse código cria novos objetos de entidade e carrega dados de exemplo. Os dados de exemplo são usados para teste. Consulte Enrollments
e CourseAssignments
para obter exemplos de como tabelas de junção muitos para muitos podem ser propagadas.
Adicionar uma migração
Compile o projeto.
Add-Migration ComplexDataModel
O comando anterior exibe 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 o comando database update
é executado, o seguinte erro é produzido:
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.
Aplicar a migração
Agora que você tem um banco de dados existente, precisa pensar sobre como aplicar as alterações futuras a ele. Este tutorial mostra duas abordagens:
- Remover e recriar o banco de dados
- Aplicar a migração ao banco de dados existente. Embora esse método seja mais complexo e demorado, é a abordagem preferencial para ambientes de produção do mundo real. Observação: essa é uma seção opcional do tutorial. Você pode remover e recriar etapas e ignorar esta seção. Se você quiser seguir as etapas nesta seção, não realize as etapas de remover e recriar.
Remover e recriar o banco de dados
O código no DbInitializer
atualizado adiciona dados de semente às novas entidades. Para forçar o EF Core a criar um novo BD, remova e atualize o BD:
No PMC (Console do Gerenciador de Pacotes), execute o seguinte comando:
Drop-Database
Update-Database
Execute Get-Help about_EntityFrameworkCore
no PMC para obter informações de ajuda.
Execute o aplicativo. A execução do aplicativo executa o método DbInitializer.Initialize
. O DbInitializer.Initialize
popula o novo BD.
Abra o BD no SSOX:
- Se o SSOX for aberto anteriormente, clique no botão Atualizar.
- Expanda o nó Tabelas. As tabelas criadas são exibidas.
Examine a tabela CourseAssignment:
- Clique com o botão direito do mouse na tabela CourseAssignment e selecione Exibir Dados.
- Verifique se a tabela CourseAssignment contém dados.
Aplicar a migração ao banco de dados existente
Esta seção é opcional. Estas etapas só funcionarão se você tiver ignorado a seção Remover e recriar o banco de dados anterior.
Quando as migrações são executadas com os dados existentes, pode haver restrições de FK que não são atendidas com os dados existentes. Com os dados de produção, é necessário executar etapas para migrar os dados existentes. Esta seção fornece um exemplo de correção de violações de restrição de FK. Não faça essas alterações de código sem um backup. Não faça essas alterações de código se você concluir a seção anterior e atualizou o banco de dados.
O arquivo {timestamp}_ComplexDataModel.cs
contém o seguinte código:
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
type: "int",
nullable: false,
defaultValue: 0);
O código anterior adiciona uma FK DepartmentID
que não permite valor nulo à tabela Course
. O BD do tutorial anterior contém linhas em Course
e, portanto, essa tabela não pode ser atualizada por migrações.
Para fazer a migração ComplexDataModel
funcionar com os dados existentes:
- Altere o código para dar à nova coluna (
DepartmentID
) um valor padrão. - Crie um departamento fictício chamado "Temp" para atuar como o departamento padrão.
Corrigir as restrições de chave estrangeira
Atualize o método Up
das classes ComplexDataModel
:
- Abra o arquivo
{timestamp}_ComplexDataModel.cs
. - Comente a linha de código que adiciona a coluna
DepartmentID
à tabelaCourse
.
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 código realçado a seguir. O novo código é inserido após o bloco .CreateTable( name: "Department"
:
migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(type: "int", nullable: true),
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(type: "datetime2", 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);
Com as alterações anteriores, as linhas Course
existentes serão relacionadas ao departamento "Temp" após a execução do método ComplexDataModel
Up
.
Um aplicativo de produção:
- Inclui código ou scripts para adicionar linhas
Department
e linhasCourse
relacionadas às novas linhasDepartment
. - Não usa o departamento "Temp" nem o valor padrão para
Course.DepartmentID
.
O próximo tutorial abrange os dados relacionados.