Часть 5. Razor Страницы с EF Core ASP.NET Core — модель данных
Авторы: Том Дайкстра (Tom Dykstra), Джереми Ликнесс (Jeremy Likness) и Йон П. Смит (Jon P Smith)
Веб-приложение Contoso University демонстрирует создание Razor веб-приложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств см. в первом руководстве серии.
При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение и сравните его код с тем, который вы создали в процессе работы с этим руководством.
Предыдущие руководства работали с базовой моделью данных, состоящей из трех сущностей. В этом руководстве рассматриваются следующие темы:
- Добавляются дополнительные сущности и связи.
- Настраивается модель данных с помощью указания правил для форматирования, проверки и сопоставления базы данных.
Готовая модель данных показана на следующем рисунке:
Следующая диаграмма базы данных создана с помощью средства Dataedo:
Чтобы создать диаграмму базы данных с помощью Dataedo, выполните следующие действия:
- Развертывание приложения в Azure
- Скачайте и установите Dataedo на своем компьютере.
- Следуйте инструкциям руководства Создание документации для Базы данных SQL Azure за 5 минут.
На предыдущей диаграмме Dataedo элемент CourseInstructor
— это таблица соединений, созданная Entity Framework. Дополнительные сведения см. в разделе Многие ко многим.
Сущность Student
Замените код в Models/Student.cs
следующим:
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; }
}
}
В приведенном выше коде добавляется свойство FullName
и добавляются следующие атрибуты к существующим свойствам:
Вычисляемое свойство FullName
FullName
— это вычисляемое свойство, которое возвращает значение, созданное путем объединения двух других свойств. FullName
не может быть задано, поэтому имеет только метод доступа get. В базе данных не создается столбец FullName
.
Атрибут DataType
[DataType(DataType.Date)]
Сейчас для дат зачисления учащихся на всех страницах отображаются время и дата, хотя важна только дата. Используя атрибуты заметок к данным, вы можете внести в код одно изменение, позволяющее исправить формат отображения на каждой странице, где отображаются эти данные.
Атрибут DataType указывает тип данных более точно по сравнению со встроенным типом базы данных. В этом случае следует отображать отобразить только дату, а не дату и время. В перечислении DataType представлено множество типов данных, таких как Date, Time, PhoneNumber, Currency, EmailAddress и других. Атрибут DataType
также обеспечивает автоматическое предоставление функций для определенных типов в приложении. Например:
- Ссылка
mailto:
дляDataType.EmailAddress
создается автоматически. - Средство выбора даты предоставляется для
DataType.Date
в большинстве браузеров.
Атрибут DataType
создает атрибуты HTML 5 data-
. Атрибуты DataType
не предназначены для проверки.
Атрибут DisplayFormat
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
DataType.Date
не задает формат отображаемой даты. По умолчанию поле даты отображается с использованием форматов, установленных в CultureInfo сервера.
С помощью атрибута DisplayFormat
можно явно указать формат даты: Параметр ApplyFormatInEditMode
указывает, что формат должен применяться к пользовательскому интерфейсу редактирования. Некоторым полям не следует использовать ApplyFormatInEditMode
. Например, обозначение денежной единицы в общем случае не должно отображаться в поле редактирования текста.
Атрибут DisplayFormat
можно использовать отдельно. Однако чаще всего DataType
рекомендуется применять вместе с атрибутом DisplayFormat
. Атрибут DataType
передает семантику данных (в отличие от способа их вывода на экран). Атрибут DataType
дает следующие преимущества, недоступные в DisplayFormat
:
- Поддержка функций HTML5 в браузере. Например, отображение элемента управления календарем, соответствующего языковому стандарту обозначения денежной единицы, ссылок электронной почты, проверки входных данных на стороне клиента.
- По умолчанию формат отображения данных в браузере определяется в соответствии с установленным языковым стандартом.
Дополнительные сведения см. в документации по вспомогательной функции тегов <input>.
Атрибут StringLength
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
С помощью атрибутов можно указать правила проверки данных и сообщения об ошибках проверки. Атрибут StringLength указывает минимальную и максимальную длину символов, разрешенных в поле данных. Представленный код задает ограничение длины имен в 50 символов. Пример, в котором задается минимальная длина строки, приводится далее.
Атрибут StringLength
также обеспечивает проверку на стороне клиента и на стороне сервера. Минимальное значение не оказывает влияния на схему базы данных.
Атрибут StringLength
не запрещает пользователю ввести пробел в качестве имени пользователя. Атрибут RegularExpression можно использовать для применения ограничений к входным данным. Например, следующий код требует, чтобы первый символ был прописной буквой, а остальные символы были буквенными:
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
В обозревателе объектов SQL Server (SSOX) откройте конструктор таблиц учащихся, дважды щелкнув таблицу Student (Учащийся).
На предыдущем изображении показана схемы для таблицы Student
. Поля имен имеют тип nvarchar(MAX)
. Когда далее в этом учебнике будет создана и применена миграция, поля имен станут nvarchar(50)
из-за атрибутов длины строки.
Атрибут Column
[Column("FirstName")]
public string FirstMidName { get; set; }
Атрибуты могут управлять, как именно классы и свойства сопоставляются с базой данных. В модели Student
атрибут Column
используется для сопоставления имени свойства FirstMidName
с "FirstName" в базе данных.
При создании базы данных имена свойств в модели используются для имен столбцов (кроме случая, когда используется атрибут Column
). Модель Student
использует FirstMidName
для поля имени, так как это поле также может содержать отчество.
С атрибутом [Column]
поле Student.FirstMidName
в модели данных сопоставляется со столбцом FirstName
таблицы Student
. Добавление атрибута Column
изменяет модель для поддержки SchoolContext
. Модель, поддерживающая SchoolContext
, больше не соответствует базе данных. Это несоответствие будет устранено путем добавления миграции далее в этом учебнике.
Атрибут Required
[Required]
Атрибут Required
делает свойства имен обязательными полями. Атрибут Required
не нужен для типов, не допускающих значения NULL, например для типов значений (таких как DateTime
, int
и double
). Типы, которые не могут принимать значение null, автоматически обрабатываются как обязательные поля.
Для применения MinimumLength
нужно использовать атрибут Required
с MinimumLength
.
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength
и Required
разрешают использовать пробелы при проверке. Используйте атрибут RegularExpression
для полного контроля над строкой.
Атрибут Display
[Display(Name = "Last Name")]
Атрибут Display
указывает, что заголовки для текстовых полей должны иметь вид "First Name" (Имя), "Last Name" (Фамилия), "Full Name" (Полное имя) и "Enrollment Date" (Дата зачисления). По умолчанию в заголовках не использовался пробел для разделения слов, например "Lastname".
Создание миграции
Запустите приложение и перейдите на страницу Students. Возникает исключение. Атрибут [Column]
приводит к тому, что EF ожидает столбец с именем FirstName
, но имя столбца в базе данных по-прежнему FirstMidName
.
Сообщение об ошибке подобно приведенному ниже.
SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:
SchoolContext
В PMC введите следующие команды для создания миграции и обновления базы данных:
Add-Migration ColumnFirstName Update-Database
Первая из этих команд выдает следующее предупреждающее сообщение:
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Это предупреждение вызвано тем, что поля имен теперь ограничены 50 символами. Если имя в базе данных имеет больше 50 символов, символы с 51-го до последнего будут потеряны.
Откройте таблицу "Student" (Учащийся) в окне SSOX:
До применения миграции столбцы имен имели тип nvarchar(MAX). Теперь столбцы имен имеют тип
nvarchar(50)
. Имя столбца изменилось сFirstMidName
наFirstName
.
- Запустите приложение и перейдите на страницу Students.
- Обратите внимание, что значения времени не вводятся и не отображаются вместе с датами.
- Выберите Create New (Создать) и попробуйте ввести имя длиной более 50 символов.
Примечание.
В следующих разделах сборка приложения на некоторых этапах приводит к возникновению ошибок компилятора. В инструкциях указано, когда производить сборку приложения.
Сущность Instructor
Создайте Models/Instructor.cs
, используя следующий код:
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; }
}
}
На одной строке могут находиться несколько атрибутов. Атрибуты HireDate
можно записать следующим образом:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Свойства навигации
Courses
и OfficeAssignment
— это свойства навигации.
Преподаватель может проводить любое количество курсов, поэтому Courses
определен как коллекция.
public ICollection<Course> Courses { get; set; }
Преподаватель может иметь не более одного кабинета, поэтому свойство OfficeAssignment
содержит одну сущность OfficeAssignment
. OfficeAssignment
имеет значение null, если кабинет не назначен.
public OfficeAssignment OfficeAssignment { get; set; }
Сущность OfficeAssignment
Создайте Models/OfficeAssignment.cs
, используя следующий код:
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; }
}
}
Атрибут Key
Атрибут [Key]
используется для идентификации свойства в качестве первичного ключа (PK), когда имя свойства отличается от classnameID
или ID
.
Между сущностями Instructor
и OfficeAssignment
действует связь один к нулю или к одному. Назначение кабинета существует только в связи с преподавателем, которому оно назначено. Первичный ключ OfficeAssignment
также является внешним ключом (FK) для сущности Instructor
. Связь "один к нулю или один к одному" возникает, когда PK в одной таблице является как PK, так и FK для другой таблицы.
EF Core не может автоматически распознаваться InstructorID
как PK OfficeAssignment
, так как InstructorID
не соответствует соглашению об именовании ID или classnameID. Таким образом, атрибут Key
используется для определения InstructorID
в качестве первичного ключа:
[Key]
public int InstructorID { get; set; }
По умолчанию EF Core ключ обрабатывается как небазовый, так как столбец предназначен для идентификации связи. Дополнительные сведения см. в статье о ключах EF.
Свойство навигации Instructor
Свойство навигации Instructor.OfficeAssignment
может иметь значение NULL, так как строка OfficeAssignment
для преподавателя может отсутствовать. Преподавателю может быть не назначен кабинет.
Свойство навигации OfficeAssignment.Instructor
всегда будет иметь сущность Instructor, так как тип внешнего ключа InstructorID
— это тип значения int
, не допускающий значения NULL. Назначение кабинета не может существовать без преподавателя.
Когда сущность Instructor
имеет связанную сущность OfficeAssignment
, каждая из них имеет ссылку на другую в своем свойстве навигации.
Сущность Course
Обновите Models/Course.cs
, включив в него следующий код.
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; }
}
}
Сущность Course
имеет свойство внешнего ключа (FK) DepartmentID
. DepartmentID
указывает на связанную сущность Department
. Сущность Course
имеет свойство навигации Department
.
EF Core Не требуется свойство внешнего ключа для модели данных, если модель имеет свойство навигации для связанной сущности. EF Core автоматически создает FK в базе данных, где бы они ни находились. EF Core создает теневые свойства для автоматически созданных FK. Однако явное включение внешнего ключа в модель данных позволяет сделать обновления проще и эффективнее. Например, рассмотрим модель, где свойство внешнего ключа DepartmentID
не включено. При получении сущности курса для редактирования:
- свойство
Department
имеет значениеnull
, если оно не загружено явно; - для обновления сущности курса нужно сначала получить сущность
Department
.
Если свойство внешнего ключа DepartmentID
включено в модель данных, получать сущность Department
перед обновлением не нужно.
Атрибут DatabaseGenerated
Атрибут [DatabaseGenerated(DatabaseGeneratedOption.None)]
указывает, что первичный ключ предоставляется приложением, а не создается базой данных.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
По умолчанию предполагается, EF Core что значения PK создаются базой данных. Обычно лучше всего использовать значения, созданные базой данных. Для сущностей Course
пользователь указывает первичный ключ. Например, номер курса, такой как серия 1000 для кафедры математики и серия 2000 для кафедры английского языка.
Атрибут DatabaseGenerated
также может использоваться для создания значений по умолчанию. Например, база данных может автоматически создать поле даты для записи даты, когда строка была создана или изменена. Дополнительные сведения см. в разделе Созданные свойства.
Свойства внешнего ключа и навигации
Свойства внешнего ключа (FK) и свойства навигации в сущности Course
отражают следующие связи:
Курс назначается одной кафедре, поэтому имеется внешний ключ DepartmentID
и свойство навигации Department
.
public int DepartmentID { get; set; }
public Department Department { get; set; }
На курс может быть зачислено любое количество учащихся, поэтому свойство навигации Enrollments
является коллекцией:
public ICollection<Enrollment> Enrollments { get; set; }
Курс могут вести несколько преподавателей, поэтому свойство навигации Instructors
является коллекцией:
public ICollection<Instructor> Instructors { get; set; }
Сущность Department
Создайте Models/Department.cs
, используя следующий код:
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; }
}
}
Атрибут Column
Ранее атрибут Column
использовался, чтобы изменить сопоставление имени столбца. В коде для сущности Department
атрибут Column
можно использовать, чтобы изменить сопоставление типов данных SQL. Столбец Budget
определяется с помощью типа money SQL Server в базе данных:
[Column(TypeName="money")]
public decimal Budget { get; set; }
Сопоставление столбцов обычно не требуется. EF Core выбирает соответствующий тип данных SQL Server на основе типа СРЕДЫ CLR для свойства. Тип decimal
среды CLR сопоставляется с типом decimal
SQL Server. Budget
используется для денежных единиц, хотя для этого лучше подходит тип данных money.
Свойства внешнего ключа и навигации
Свойства внешнего ключа и навигации отражают следующие связи:
- Кафедра может иметь или не иметь администратора.
- Администратор всегда является преподавателем. Поэтому свойство
InstructorID
включается в качестве внешнего ключа в сущностьInstructor
.
Свойство навигации называется Administrator
, но содержит сущность Instructor
:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
Вопросительный знак ?
в приведенном выше коде указывает, что свойство допускает значение NULL.
Кафедра может иметь несколько курсов, поэтому доступно свойство навигации Courses:
public ICollection<Course> Courses { get; set; }
По соглашению EF Core включает каскадное удаление для ненулевого FK и для связей "многие ко многим". Это поведение по умолчанию может привести к циклическим правилам каскадного удаления. Такие правила вызывают исключение при добавлении миграции.
Например, если Department.InstructorID
свойство было определено как ненулевое, EF Core настройте правило каскадного удаления. В этом случае кафедра будет удалена, если будет удален преподаватель, назначенный ее администратором. В такой ситуации правило ограничения будет более целесообразным. Приведенный ниже текучий API задает правило ограничения и отключает правило каскадного удаления.
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
Свойства внешнего ключа и навигации сущности Enrollment
Запись зачисления обозначает один курс, который проходит один учащийся.
Обновите Models/Enrollment.cs
, включив в него следующий код.
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; }
}
}
Свойства внешнего ключа и навигации отражают следующие связи:
Запись зачисления предназначена для одного курса, поэтому доступно свойство первичного ключа CourseID
и свойство навигации Course
:
public int CourseID { get; set; }
public Course Course { get; set; }
Запись зачисления предназначена для одного учащегося, поэтому доступно свойство первичного ключа StudentID
и свойство навигации Student
:
public int StudentID { get; set; }
public Student Student { get; set; }
Связи «многие ко многим»
Между сущностями Student
и Course
действует связь многие ко многим. Сущности Enrollment
выступают в качестве таблицы соединения "многие ко многим" с полезными данными в базе данных. Фраза с полезными данными означает, что таблица Enrollment
содержит дополнительные данные, кроме внешних ключей для присоединяемых таблиц. В сущности Enrollment
дополнительными данными помимо внешних ключей являются PK и Grade
.
На следующем рисунке показано, как выглядят эти связи на схеме сущностей. (Эта схема была создана с помощью EF Power Tools для EF 6.x. Процедура ее создания не входит в этот учебник.)
Каждая линия связи имеет 1 на одном конце и звездочку (*) на другом, указывая характер один ко многим.
Если таблица Enrollment
не включала в себя сведения об оценках, потребуется, чтобы она содержала всего два внешних ключа: CourseID
и StudentID
. Таблицу соединения многие ко многим без полезных данных иногда называют чистой таблицей соединения (PJT).
Сущности Instructor
и Course
имеют связь "многие ко многим" с использованием PJT.
Обновление контекста базы данных
Обновите Data/SchoolContext.cs
, включив в него следующий код.
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
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));
}
}
}
Приведенный выше код добавляет новые сущности и настраивает связь "многие ко многим" между сущностями Instructor
и Course
.
Текучий API вместо атрибутов
Метод OnModelCreating
в предыдущем коде использует api fluent для настройки EF Core поведения. Этот API называется "текучим", так как часто используется для объединения серии вызовов методов в один оператор. В следующем коде показан пример текучего API:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
В этом учебнике текучий API используется только для сопоставления базы данных, которое невозможно выполнить с помощью атрибутов. Однако текучий API позволяет задать большинство правил форматирования, проверки и сопоставления, которые можно указать с помощью атрибутов.
Некоторые атрибуты, такие как MinimumLength
, невозможно применить с текучим API. MinimumLength
не изменяет схему, он лишь применяет правило проверки минимальной длины.
Некоторые разработчики предпочитают использовать текучий API монопольно, чтобы оставить свои классы сущностей чистыми. Атрибуты и текучий API можно смешивать. Существует несколько конфигураций, которые можно реализовать только с помощью текучего API, например указание составного первичного ключа. Существует несколько конфигураций, которые можно реализовать только с помощью атрибутов (MinimumLength
). Рекомендации по использованию текучего API и атрибутов:
- Выберите один из двух этих подходов.
- Используйте выбранный подход максимально согласованно.
Некоторые атрибуты из этого руководства используются:
- только для проверки (например,
MinimumLength
); - EF Core только конфигурация (например,
HasKey
). - Проверка и EF Core настройка (например,
[StringLength(50)]
).
Дополнительные сведения о сравнении атрибутов и текучего API см. в разделе Методы конфигурации.
Заполнение базы данных
Обновите код в 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();
}
}
}
Предыдущий код предоставляет начальные данные для новых сущностей. Основная часть кода создает объекты сущностей и загружает демонстрационные данные. Демонстрационные данные используются для проверки.
Применение миграции либо удаление и повторное создание
Существующую базу данных можно изменить, использую один из следующих двух подходов.
- Удаление и повторное создание базы данных. Выберите этот раздел, при использовании SQLite.
- Применение миграции к существующей базе данных. Инструкции в этом разделе подходят только для SQL Server, но не для SQLite.
Для SQL Server применимы оба подхода. Хотя метод с применением миграции является более сложным и трудоемким, в реальной рабочей среде лучше использовать именно его.
Удаление и повторное создание базы данных
Чтобы принудительно EF Core создать новую базу данных, удалите и обновите базу данных:
- Удалите папку Migrations.
- Выполните следующие команды в консоли диспетчера пакетов (PMC):
Drop-Database
Add-Migration InitialCreate
Update-Database
Выполнить приложение. При запуске приложения выполняется метод DbInitializer.Initialize
. DbInitializer.Initialize
заполняет новую базу данных.
Откройте базу данных в SSOX.
- Если SSOX был открыт ранее, нажмите кнопку Обновить.
- Разверните узел Таблицы. Отображаются созданные таблицы.
Следующие шаги
В следующих двух учебниках рассказывается о том, как считывать и обновлять связанные данные.
Предыдущие руководства работали с базовой моделью данных, состоящей из трех сущностей. В этом руководстве рассматриваются следующие темы:
- Добавляются дополнительные сущности и связи.
- Настраивается модель данных с помощью указания правил для форматирования, проверки и сопоставления базы данных.
Готовая модель данных показана на следующем рисунке:
Сущность Student
Замените код в Models/Student.cs
следующим:
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; }
}
}
В приведенном выше коде добавляется свойство FullName
и добавляются следующие атрибуты к существующим свойствам:
[DataType]
[DisplayFormat]
[StringLength]
[Column]
[Required]
[Display]
Вычисляемое свойство FullName
FullName
— это вычисляемое свойство, которое возвращает значение, созданное путем объединения двух других свойств. FullName
не может быть задано, поэтому имеет только метод доступа get. В базе данных не создается столбец FullName
.
Атрибут DataType
[DataType(DataType.Date)]
Сейчас для дат зачисления учащихся на всех страницах отображаются время и дата, хотя важна только дата. Используя атрибуты заметок к данным, вы можете внести в код одно изменение, позволяющее исправить формат отображения на каждой странице, где отображаются эти данные.
Атрибут DataType указывает тип данных более точно по сравнению со встроенным типом базы данных. В этом случае следует отображать отобразить только дату, а не дату и время. В перечислении DataType представлено множество типов данных, таких как Date, Time, PhoneNumber, Currency, EmailAddress и других. Атрибут DataType
также обеспечивает автоматическое предоставление функций для определенных типов в приложении. Например:
- Ссылка
mailto:
дляDataType.EmailAddress
создается автоматически. - Средство выбора даты предоставляется для
DataType.Date
в большинстве браузеров.
Атрибут DataType
создает атрибуты HTML 5 data-
. Атрибуты DataType
не предназначены для проверки.
Атрибут DisplayFormat
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
DataType.Date
не задает формат отображаемой даты. По умолчанию поле даты отображается с использованием форматов, установленных в CultureInfo сервера.
С помощью атрибута DisplayFormat
можно явно указать формат даты: Параметр ApplyFormatInEditMode
указывает, что формат должен применяться к пользовательскому интерфейсу редактирования. Некоторым полям не следует использовать ApplyFormatInEditMode
. Например, обозначение денежной единицы в общем случае не должно отображаться в поле редактирования текста.
Атрибут DisplayFormat
можно использовать отдельно. Однако чаще всего DataType
рекомендуется применять вместе с атрибутом DisplayFormat
. Атрибут DataType
передает семантику данных (в отличие от способа их вывода на экран). Атрибут DataType
дает следующие преимущества, недоступные в DisplayFormat
:
- Поддержка функций HTML5 в браузере. Например, отображение элемента управления календарем, соответствующего языковому стандарту обозначения денежной единицы, ссылок электронной почты, проверки входных данных на стороне клиента.
- По умолчанию формат отображения данных в браузере определяется в соответствии с установленным языковым стандартом.
Дополнительные сведения см. в документации по вспомогательной функции тегов <input>.
Атрибут StringLength
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
С помощью атрибутов можно указать правила проверки данных и сообщения об ошибках проверки. Атрибут StringLength указывает минимальную и максимальную длину символов, разрешенных в поле данных. Представленный код задает ограничение длины имен в 50 символов. Пример, в котором задается минимальная длина строки, приводится далее.
Атрибут StringLength
также обеспечивает проверку на стороне клиента и на стороне сервера. Минимальное значение не оказывает влияния на схему базы данных.
Атрибут StringLength
не запрещает пользователю ввести пробел в качестве имени пользователя. Атрибут RegularExpression можно использовать для применения ограничений к входным данным. Например, следующий код требует, чтобы первый символ был прописной буквой, а остальные символы были буквенными:
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
В обозревателе объектов SQL Server (SSOX) откройте конструктор таблиц учащихся, дважды щелкнув таблицу Student (Учащийся).
На предыдущем изображении показана схемы для таблицы Student
. Поля имен имеют тип nvarchar(MAX)
. Когда далее в этом учебнике будет создана и применена миграция, поля имен станут nvarchar(50)
из-за атрибутов длины строки.
Атрибут Column
[Column("FirstName")]
public string FirstMidName { get; set; }
Атрибуты могут управлять, как именно классы и свойства сопоставляются с базой данных. В модели Student
атрибут Column
используется для сопоставления имени свойства FirstMidName
с "FirstName" в базе данных.
При создании базы данных имена свойств в модели используются для имен столбцов (кроме случая, когда используется атрибут Column
). Модель Student
использует FirstMidName
для поля имени, так как это поле также может содержать отчество.
С атрибутом [Column]
поле Student.FirstMidName
в модели данных сопоставляется со столбцом FirstName
таблицы Student
. Добавление атрибута Column
изменяет модель для поддержки SchoolContext
. Модель, поддерживающая SchoolContext
, больше не соответствует базе данных. Это несоответствие будет устранено путем добавления миграции далее в этом учебнике.
Атрибут Required
[Required]
Атрибут Required
делает свойства имен обязательными полями. Атрибут Required
не нужен для типов, не допускающих значения NULL, например для типов значений (таких как DateTime
, int
и double
). Типы, которые не могут принимать значение null, автоматически обрабатываются как обязательные поля.
Для применения MinimumLength
нужно использовать атрибут Required
с MinimumLength
.
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength
и Required
разрешают использовать пробелы при проверке. Используйте атрибут RegularExpression
для полного контроля над строкой.
Атрибут Display
[Display(Name = "Last Name")]
Атрибут Display
указывает, что заголовки для текстовых полей должны иметь вид "First Name" (Имя), "Last Name" (Фамилия), "Full Name" (Полное имя) и "Enrollment Date" (Дата зачисления). По умолчанию в заголовках не использовался пробел для разделения слов, например "Lastname".
Создание миграции
Запустите приложение и перейдите на страницу Students. Возникает исключение. Атрибут [Column]
приводит к тому, что EF ожидает столбец с именем FirstName
, но имя столбца в базе данных по-прежнему FirstMidName
.
Сообщение об ошибке подобно приведенному ниже.
SqlException: Invalid column name 'FirstName'.
В PMC введите следующие команды для создания миграции и обновления базы данных:
Add-Migration ColumnFirstName Update-Database
Первая из этих команд выдает следующее предупреждающее сообщение:
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Это предупреждение вызвано тем, что поля имен теперь ограничены 50 символами. Если имя в базе данных имеет больше 50 символов, символы с 51-го до последнего будут потеряны.
Откройте таблицу "Student" (Учащийся) в окне SSOX:
До применения миграции столбцы имен имели тип nvarchar(MAX). Теперь столбцы имен имеют тип
nvarchar(50)
. Имя столбца изменилось сFirstMidName
наFirstName
.
- Запустите приложение и перейдите на страницу Students.
- Обратите внимание, что значения времени не вводятся и не отображаются вместе с датами.
- Выберите Create New (Создать) и попробуйте ввести имя длиной более 50 символов.
Примечание.
В следующих разделах сборка приложения на некоторых этапах приводит к возникновению ошибок компилятора. В инструкциях указано, когда производить сборку приложения.
Сущность Instructor
Создайте Models/Instructor.cs
, используя следующий код:
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; }
}
}
На одной строке могут находиться несколько атрибутов. Атрибуты HireDate
можно записать следующим образом:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Свойства навигации
CourseAssignments
и OfficeAssignment
— это свойства навигации.
Преподаватель может проводить любое количество курсов, поэтому CourseAssignments
определен как коллекция.
public ICollection<CourseAssignment> CourseAssignments { get; set; }
Преподаватель может иметь не более одного кабинета, поэтому свойство OfficeAssignment
содержит одну сущность OfficeAssignment
. OfficeAssignment
имеет значение null, если кабинет не назначен.
public OfficeAssignment OfficeAssignment { get; set; }
Сущность OfficeAssignment
Создайте Models/OfficeAssignment.cs
, используя следующий код:
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; }
}
}
Атрибут Key
Атрибут [Key]
используется для идентификации свойства в качестве первичного ключа (PK), когда имя свойства отличается от classnameID или ID.
Между сущностями Instructor
и OfficeAssignment
действует связь один к нулю или к одному. Назначение кабинета существует только в связи с преподавателем, которому оно назначено. Первичный ключ OfficeAssignment
также является внешним ключом (FK) для сущности Instructor
.
EF Core не может автоматически распознаваться InstructorID
как PK OfficeAssignment
, так как InstructorID
не соответствует соглашению об именовании ID или classnameID. Таким образом, атрибут Key
используется для определения InstructorID
в качестве первичного ключа:
[Key]
public int InstructorID { get; set; }
По умолчанию EF Core ключ обрабатывается как небазовый, так как столбец предназначен для идентификации связи.
Свойство навигации Instructor
Свойство навигации Instructor.OfficeAssignment
может иметь значение NULL, так как строка OfficeAssignment
для преподавателя может отсутствовать. Преподавателю может быть не назначен кабинет.
Свойство навигации OfficeAssignment.Instructor
всегда будет иметь сущность Instructor, так как тип внешнего ключа InstructorID
— это тип значения int
, не допускающий значения NULL. Назначение кабинета не может существовать без преподавателя.
Когда сущность Instructor
имеет связанную сущность OfficeAssignment
, каждая из них имеет ссылку на другую в своем свойстве навигации.
Сущность Course
Обновите Models/Course.cs
, включив в него следующий код.
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; }
}
}
Сущность Course
имеет свойство внешнего ключа (FK) DepartmentID
. DepartmentID
указывает на связанную сущность Department
. Сущность Course
имеет свойство навигации Department
.
EF Core Не требуется свойство внешнего ключа для модели данных, если модель имеет свойство навигации для связанной сущности. EF Core автоматически создает FK в базе данных, где бы они ни находились. EF Core создает теневые свойства для автоматически созданных FK. Однако явное включение внешнего ключа в модель данных позволяет сделать обновления проще и эффективнее. Например, рассмотрим модель, где свойство внешнего ключа DepartmentID
не включено. При получении сущности курса для редактирования:
- свойство
Department
имеет значение NULL, если оно не загружено явно; - для обновления сущности курса нужно сначала получить сущность
Department
.
Если свойство внешнего ключа DepartmentID
включено в модель данных, получать сущность Department
перед обновлением не нужно.
Атрибут DatabaseGenerated
Атрибут [DatabaseGenerated(DatabaseGeneratedOption.None)]
указывает, что первичный ключ предоставляется приложением, а не создается базой данных.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
По умолчанию предполагается, EF Core что значения PK создаются базой данных. Обычно лучше всего использовать значения, созданные базой данных. Для сущностей Course
пользователь указывает первичный ключ. Например, номер курса, такой как серия 1000 для кафедры математики и серия 2000 для кафедры английского языка.
Атрибут DatabaseGenerated
также может использоваться для создания значений по умолчанию. Например, база данных может автоматически создать поле даты для записи даты, когда строка была создана или изменена. Дополнительные сведения см. в разделе Созданные свойства.
Свойства внешнего ключа и навигации
Свойства внешнего ключа (FK) и свойства навигации в сущности Course
отражают следующие связи:
Курс назначается одной кафедре, поэтому имеется внешний ключ DepartmentID
и свойство навигации Department
.
public int DepartmentID { get; set; }
public Department Department { get; set; }
На курс может быть зачислено любое количество учащихся, поэтому свойство навигации Enrollments
является коллекцией:
public ICollection<Enrollment> Enrollments { get; set; }
Курс могут вести несколько преподавателей, поэтому свойство навигации CourseAssignments
является коллекцией:
public ICollection<CourseAssignment> CourseAssignments { get; set; }
CourseAssignment
описано далее.
Сущность Department
Создайте Models/Department.cs
, используя следующий код:
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; }
}
}
Атрибут Column
Ранее атрибут Column
использовался, чтобы изменить сопоставление имени столбца. В коде для сущности Department
атрибут Column
можно использовать, чтобы изменить сопоставление типов данных SQL. Столбец Budget
определяется с помощью типа money SQL Server в базе данных:
[Column(TypeName="money")]
public decimal Budget { get; set; }
Сопоставление столбцов обычно не требуется. EF Core выбирает соответствующий тип данных SQL Server на основе типа СРЕДЫ CLR для свойства. Тип decimal
среды CLR сопоставляется с типом decimal
SQL Server. Budget
используется для денежных единиц, хотя для этого лучше подходит тип данных money.
Свойства внешнего ключа и навигации
Свойства внешнего ключа и навигации отражают следующие связи:
- Кафедра может иметь или не иметь администратора.
- Администратор всегда является преподавателем. Поэтому свойство
InstructorID
включается в качестве внешнего ключа в сущностьInstructor
.
Свойство навигации называется Administrator
, но содержит сущность Instructor
:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
Вопросительный знак (?) в приведенном выше коде указывает, что свойство допускает значение null.
Кафедра может иметь несколько курсов, поэтому доступно свойство навигации Courses:
public ICollection<Course> Courses { get; set; }
По соглашению EF Core включает каскадное удаление для ненулевого FK и для связей "многие ко многим". Это поведение по умолчанию может привести к циклическим правилам каскадного удаления. Такие правила вызывают исключение при добавлении миграции.
Например, если Department.InstructorID
свойство было определено как ненулевое, EF Core настройте правило каскадного удаления. В этом случае кафедра будет удалена, если будет удален преподаватель, назначенный ее администратором. В такой ситуации правило ограничения будет более целесообразным. Приведенный ниже текучий API задает правило ограничения и отключает правило каскадного удаления.
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
Сущность Enrollment
Запись зачисления обозначает один курс, который проходит один учащийся.
Обновите Models/Enrollment.cs
, включив в него следующий код.
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; }
}
}
Свойства внешнего ключа и навигации
Свойства внешнего ключа и навигации отражают следующие связи:
Запись зачисления предназначена для одного курса, поэтому доступно свойство первичного ключа CourseID
и свойство навигации Course
:
public int CourseID { get; set; }
public Course Course { get; set; }
Запись зачисления предназначена для одного учащегося, поэтому доступно свойство первичного ключа StudentID
и свойство навигации Student
:
public int StudentID { get; set; }
public Student Student { get; set; }
Связи «многие ко многим»
Между сущностями Student
и Course
действует связь многие ко многим. Сущности Enrollment
выступают в качестве таблицы соединения "многие ко многим" с полезными данными в базе данных. Фраза "с полезными данными" означает, что таблица Enrollment
содержит дополнительные данные, кроме внешних ключей для присоединяемых таблиц (в данном случае — первичный ключ и Grade
).
На следующем рисунке показано, как выглядят эти связи на схеме сущностей. (Эта схема была создана с помощью EF Power Tools для EF 6.x. Процедура ее создания не входит в этот учебник.)
Каждая линия связи имеет 1 на одном конце и звездочку (*) на другом, указывая характер один ко многим.
Если таблица Enrollment
не включала в себя сведения об оценках, ей потребуется содержать всего два внешних ключа (CourseID
и StudentID
). Таблицу соединения многие ко многим без полезных данных иногда называют чистой таблицей соединения (PJT).
Сущности Instructor
и Course
имеют связь многие ко многим с использованием чистой таблицы соединения.
Примечание. EF 6.x поддерживает неявные таблицы соединения для связей "многие ко многим", но EF Core не поддерживаются. Дополнительные сведения см. в EF Core разделе "Многие ко многим" в версии 2.0.
Сущность CourseAssignment
Создайте Models/CourseAssignment.cs
, используя следующий код:
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; }
}
}
Для связи "многие ко многим" между Instructor и Courses нужна таблица соединения, сущностью для которой является CourseAssignment.
Обычно для сущности соединения используется имя EntityName1EntityName2
. Например, таблицей соединения Instructor и Courses, использующей этот шаблон, будет CourseInstructor
. Однако рекомендуется использовать имя, которое описывает эту связь.
Модели данных создаются простыми и разрастаются. Таблицы соединения без полезных данных (PJT) часто начинают включать их. Если в начале задать описательное имя сущности, его не нужно менять при изменениях таблицы соединения. Оптимально, если сущность соединения имеет собственное естественное имя (возможно, из одного слова) в бизнес-среде. Например, Books и Customers можно связать с сущностью соединения Ratings. Связь многие ко многим между Instructor и Courses CourseAssignment
является более предпочтительным вариантом, чем CourseInstructor
.
Составной ключ
Два внешних ключа в CourseAssignment
(InstructorID
и CourseID
) совместно однозначно определяют каждую строку таблицы CourseAssignment
. CourseAssignment
не требуется выделенный первичный ключ. Свойства InstructorID
и CourseID
выступают в качестве составного первичного ключа. Единственным способом указания составных PK EF Core является использование api fluent. Следующий раздел описывает, как настроить составной первичный ключ.
Составной ключ обеспечивает следующее:
- Для одного курса допускается несколько строк.
- Для одного преподавателя допускается несколько строк.
- Несколько строк для одного преподавателя и курса недопустимы.
Сущность соединения Enrollment
определяет собственный первичный ключ, поэтому подобные дубликаты возможны. Для предотвращения подобных дубликатов:
- добавьте уникальный индекс для полей внешнего ключа или
- настройте
Enrollment
с первичным составным ключом аналогичноCourseAssignment
. Дополнительные сведения см. в разделе Индексы.
Обновление контекста базы данных
Обновите Data/SchoolContext.cs
, включив в него следующий код.
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
public DbSet<CourseAssignment> CourseAssignments { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
Предыдущий код добавляет новые сущности и настраивает составной первичный ключ сущности CourseAssignment
.
Текучий API вместо атрибутов
Метод OnModelCreating
в предыдущем коде использует api fluent для настройки EF Core поведения. Этот API называется "текучим", так как часто используется для объединения серии вызовов методов в один оператор. В следующем коде показан пример текучего API:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
В этом учебнике текучий API используется только для сопоставления базы данных, которое невозможно выполнить с помощью атрибутов. Однако текучий API позволяет задать большинство правил форматирования, проверки и сопоставления, которые можно указать с помощью атрибутов.
Некоторые атрибуты, такие как MinimumLength
, невозможно применить с текучим API. MinimumLength
не изменяет схему, он лишь применяет правило проверки минимальной длины.
Некоторые разработчики предпочитают использовать текучий API монопольно, чтобы оставить свои классы сущностей "чистыми". Атрибуты и текучий API можно смешивать. Существует несколько конфигураций, которые можно реализовать только с помощью текучего API (с указанием составного первичного ключа). Существует несколько конфигураций, которые можно реализовать только с помощью атрибутов (MinimumLength
). Рекомендации по использованию текучего API и атрибутов:
- Выберите один из двух этих подходов.
- Используйте выбранный подход максимально согласованно.
Некоторые атрибуты из этого руководства используются:
- только для проверки (например,
MinimumLength
); - EF Core только конфигурация (например,
HasKey
). - Проверка и EF Core настройка (например,
[StringLength(50)]
).
Дополнительные сведения о сравнении атрибутов и текучего API см. в разделе Методы конфигурации.
Схема сущностей
Ниже показана схема, создаваемая средствами EF Power Tools для завершенной модели School.
На предыдущей схеме показано следующее:
- Несколько линий связи один ко многим (1 к *).
- Линия связи один к нулю или одному (1 к 0..1) между сущностями
Instructor
иOfficeAssignment
. - Линия связи нуль или один ко многим (0..1 к *) между сущностями
Instructor
иDepartment
.
Заполнение базы данных
Обновите код в 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();
}
}
}
Предыдущий код предоставляет начальные данные для новых сущностей. Основная часть кода создает объекты сущностей и загружает демонстрационные данные. Демонстрационные данные используются для проверки. См. Enrollments
и CourseAssignments
, чтобы ознакомиться с примерами заполнения данными таблиц соединения "многие ко многим".
Добавление миграции
Выполните сборку проекта.
В PMC выполните следующую команду:
Add-Migration ComplexDataModel
Предыдущая команда отображает предупреждение о возможной потере данных.
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'
При выполнении команды database update
возникает следующая ошибка:
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'.
В следующем разделе вы узнаете, что делать с этой ошибкой.
Применение миграции либо удаление и повторное создание
Теперь у вас есть база данных, и пора подумать о том, как применять к ней изменения. В этом руководстве показано два подхода:
- Удаление и повторное создание базы данных. Выберите этот раздел, если вы используете SQLite.
- Применение миграции к существующей базе данных. Инструкции в этом разделе подходят только для SQL Server, но не для SQLite.
Для SQL Server применимы оба подхода. Хотя метод с применением миграции является более сложным и трудоемким, в реальной рабочей среде лучше использовать именно его.
Удаление и повторное создание базы данных
Пропустите этот раздел, если вы используете SQL Server и хотите применить миграцию в следующем разделе.
Чтобы принудительно EF Core создать новую базу данных, удалите и обновите базу данных:
Выполните следующие команды в консоли диспетчера пакетов (PMC):
Drop-Database
Удалите папку Migrations, а затем выполните следующую команду:
Add-Migration InitialCreate Update-Database
Выполнить приложение. При запуске приложения выполняется метод DbInitializer.Initialize
. DbInitializer.Initialize
заполняет новую базу данных.
Откройте базу данных в SSOX.
Если SSOX был открыт ранее, нажмите кнопку Обновить.
Разверните узел Таблицы. Отображаются созданные таблицы.
Изучите таблицу CourseAssignment:
- Щелкните правой кнопкой мыши таблицу CourseAssignment и выберите пункт Просмотреть данные.
- Убедитесь, что таблица CourseAssignment содержит данные.
Применение миграции
Это необязательный раздел. Эти действия подходят только для SQL Server LocalDB и только в том случае, если вы пропустили предыдущий раздел Удаление и повторное создание базы данных.
При выполнении миграций с существующими данными могут действовать ограничения внешнего ключа, которым существующие данные не соответствуют. Для рабочих данных нужно предпринять меры по переносу существующих данных. Этот раздел содержит пример того, как устранить нарушения ограничений внешнего ключа. Вносите эти изменения кода только после создания резервной копии. Не вносите эти изменения в код, если вы выполнили инструкции из предыдущего раздела Удаление и повторное создание базы данных.
Файл {timestamp}_ComplexDataModel.cs
содержит следующий код:
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
type: "int",
nullable: false,
defaultValue: 0);
Предыдущий код добавляет в таблицу Course
внешний ключ DepartmentID
, не допускающий значение null. База данных из предыдущего учебника содержит строки в Course
, поэтому эту таблицу невозможно обновить с помощью миграций.
Чтобы заставить миграцию ComplexDataModel
работать с существующими данными:
- Изменить код, чтобы присвоить новому столбцу (
DepartmentID
) значение по умолчанию. - Создайте фиктивную кафедру с именем "Temp" для использования по умолчанию.
Устранение ограничений внешнего ключа
В классе миграции Up
обновите метод ComplexDataModel
:
- Откройте файл
{timestamp}_ComplexDataModel.cs
. - Закомментируйте строку кода, которая добавляет столбец
DepartmentID
в таблицуCourse
.
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);
Добавьте выделенный ниже код. Новый код идет после блока .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);
После внесения описанных выше изменений существующие строки Course
будут связаны с кафедрой "Temp" после выполнения метода ComplexDataModel.Up
.
Инструкции на случай описанной здесь ситуации упрощены в этом учебнике. Рабочее приложение:
- включает код или сценарии, чтобы добавить строки
Department
и связанные строкиCourse
к новым строкамDepartment
; - не использует кафедру "Temp" или значение по умолчанию для
Course.DepartmentID
.
Выполните следующие команды в консоли диспетчера пакетов (PMC):
Update-Database
Так как метод DbInitializer.Initialize
предназначен для работы только с пустой базой данных, используйте SSOX для удаления всех строк в таблицах Student и Course. (К таблице Enrollment будет применено каскадное удаление.)
Выполнить приложение. При запуске приложения выполняется метод DbInitializer.Initialize
. DbInitializer.Initialize
заполняет новую базу данных.
Следующие шаги
В следующих двух учебниках рассказывается о том, как считывать и обновлять связанные данные.
Предыдущие руководства работали с базовой моделью данных, состоящей из трех сущностей. В этом руководстве рассматриваются следующие темы:
- Добавляются дополнительные сущности и связи.
- Настраивается модель данных с помощью указания правил для форматирования, проверки и сопоставления базы данных.
Классы сущностей для готовой модели данных показаны на следующем рисунке:
При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение.
Настройка модели данных с использованием атрибутов
В этом разделе модель данных настраивается с помощью атрибутов.
Атрибут DataType
Страницы учащихся сейчас отображают время и дату зачисления. Как правило, поля даты отображают только дату без времени.
Обновите Models/Student.cs
следующий выделенный код:
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; }
}
}
Атрибут DataType указывает тип данных более точно по сравнению со встроенным типом базы данных. В этом случае следует отображать отобразить только дату, а не дату и время. В перечислении DataType представлено множество типов данных, таких как Date, Time, PhoneNumber, Currency, EmailAddress и других. Атрибут DataType
также обеспечивает автоматическое предоставление функций для определенных типов в приложении. Например:
- Ссылка
mailto:
дляDataType.EmailAddress
создается автоматически. - Средство выбора даты предоставляется для
DataType.Date
в большинстве браузеров.
Атрибут DataType
создает атрибуты HTML 5 data-
, которые используются браузерами с поддержкой HTML 5. Атрибуты DataType
не предназначены для проверки.
DataType.Date
не задает формат отображаемой даты. По умолчанию поле даты отображается с использованием форматов, установленных в CultureInfo сервера.
С помощью атрибута DisplayFormat
можно явно указать формат даты:
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Параметр ApplyFormatInEditMode
указывает, что формат должен применяться к пользовательскому интерфейсу редактирования. Некоторым полям не следует использовать ApplyFormatInEditMode
. Например, обозначение денежной единицы в общем случае не должно отображаться в поле редактирования текста.
Атрибут DisplayFormat
можно использовать отдельно. Однако чаще всего DataType
рекомендуется применять вместе с атрибутом DisplayFormat
. Атрибут DataType
передает семантику данных (в отличие от способа их вывода на экран). Атрибут DataType
дает следующие преимущества, недоступные в DisplayFormat
:
- Поддержка функций HTML5 в браузере. Например, отображение элемента управления календарем, соответствующего языковому стандарту обозначения денежной единицы, ссылок электронной почты, проверки входных данных на стороне клиента и т. д.
- По умолчанию формат отображения данных в браузере определяется в соответствии с установленным языковым стандартом.
Дополнительные сведения см. в документации по вспомогательной функции тегов <input>.
Выполнить приложение. Перейдите на страницу индекса учащихся. Время больше не отображается. Каждое представление, использующее модель Student
, отображает дату без времени.
Атрибут StringLength
С помощью атрибутов можно указать правила проверки данных и сообщения об ошибках проверки. Атрибут StringLength указывает минимальную и максимальную длину символов, разрешенных в поле данных. Атрибут StringLength
также обеспечивает проверку на стороне клиента и на стороне сервера. Минимальное значение не оказывает влияния на схему базы данных.
Обновите модельStudent
, используя следующий код:
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; }
}
}
Предыдущий код задает ограничение длины имен в 50 символов. Атрибут StringLength
не запрещает пользователю ввести пробел в качестве имени пользователя. Атрибут RegularExpression используется для применения ограничений к входным данным. Например, следующий код требует, чтобы первый символ был прописной буквой, а остальные символы были буквенными:
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
Запустите приложение:
- Перейдите на страницу учащихся.
- Выберите Create New (Создать) и введите имя длиной более 50 символов.
- Выберите Create (Создать), проверка на стороне клиента отображает сообщение об ошибке.
В обозревателе объектов SQL Server (SSOX) откройте конструктор таблиц учащихся, дважды щелкнув таблицу Student (Учащийся).
На предыдущем изображении показана схемы для таблицы Student
. Поля имен имеют тип nvarchar(MAX)
, так как миграции для базы данных не выполнялись. Когда в дальнейшем миграции будут выполнены, поля имен станут nvarchar(50)
.
Атрибут Column
Атрибуты могут управлять, как именно классы и свойства сопоставляются с базой данных. В этом разделе атрибут Column
используется для сопоставления имени свойства FirstMidName
с "FirstName" в базе данных.
При создании базы данных имена свойств в модели используются для имен столбцов (кроме случая, когда используется атрибут Column
).
Модель Student
использует FirstMidName
для поля имени, так как это поле также может содержать отчество.
Student.cs
Обновите файл со следующим выделенным кодом:
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; }
}
}
С учетом предыдущего изменения Student.FirstMidName
в приложении сопоставляется со столбцом FirstName
таблицы Student
.
Добавление атрибута Column
изменяет модель для поддержки SchoolContext
. Модель, поддерживающая SchoolContext
, больше не соответствует базе данных. Если приложение запускается перед применением миграций, возникает следующее исключение:
SqlException: Invalid column name 'FirstName'.
Для обновления базы данных сделайте следующее:
- Выполните сборку проекта.
- Откройте командное окно в папке проекта. Введите следующие команды для создания миграции и обновления базы данных:
Add-Migration ColumnFirstName
Update-Database
Команда migrations add ColumnFirstName
выдает следующее предупреждающее сообщение:
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
Это предупреждение вызвано тем, что поля имен теперь ограничены 50 символами. Если имя в базе данных имеет больше 50 символов, символы с 51 до последнего будут потеряны.
- Тестирование приложения.
Откройте таблицу "Student" (Учащийся) в окне SSOX:
До применения миграции столбцы имен имели тип nvarchar(MAX). Теперь столбцы имен имеют тип nvarchar(50)
. Имя столбца изменилось с FirstMidName
на FirstName
.
Примечание.
В следующем разделе сборка приложения на некоторых этапах приводит к возникновению ошибок компилятора. В инструкциях указано, когда производить сборку приложения.
Обновление сущности Student
Обновите Models/Student.cs
, включив в него следующий код.
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; }
}
}
Атрибут Required
Атрибут Required
делает свойства имен обязательными полями. Атрибут Required
не нужен для типов, не допускающих значения null, например для типов значений (DateTime
, int
, double
и т. д.). Типы, которые не могут принимать значение null, автоматически обрабатываются как обязательные поля.
Атрибут Required
можно заменить параметром минимальной длины в атрибуте StringLength
:
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
Атрибут Display
Атрибут Display
указывает, что заголовки для текстовых полей должны иметь вид "First Name" (Имя), "Last Name" (Фамилия), "Full Name" (Полное имя) и "Enrollment Date" (Дата зачисления). По умолчанию в заголовках не использовался пробел для разделения слов, например "Lastname".
Вычисляемое свойство FullName
FullName
— это вычисляемое свойство, которое возвращает значение, созданное путем объединения двух других свойств. FullName
не может быть задано, оно имеет только метод доступа get. В базе данных не создается столбец FullName
.
Создание сущности Instructor
Создайте Models/Instructor.cs
, используя следующий код:
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; }
}
}
На одной строке могут находиться несколько атрибутов. Атрибуты HireDate
можно записать следующим образом:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Свойства навигации CourseAssignments и OfficeAssignment
CourseAssignments
и OfficeAssignment
— это свойства навигации.
Преподаватель может проводить любое количество курсов, поэтому CourseAssignments
определен как коллекция.
public ICollection<CourseAssignment> CourseAssignments { get; set; }
Если свойство навигации содержит несколько сущностей:
- Оно должно быть типом списка, где можно добавлять, удалять и обновлять записи.
К типам свойств навигации относятся следующие:
ICollection<T>
List<T>
HashSet<T>
Если ICollection<T>
задано, EF Core создается HashSet<T>
коллекция по умолчанию.
Сущность CourseAssignment
описана в разделе по связям многие ко многим.
Бизнес-правила университета Contoso указывают, что преподаватель может иметь не более одного кабинета. Свойство OfficeAssignment
содержит отдельную сущность OfficeAssignment
. OfficeAssignment
имеет значение null, если кабинет не назначен.
public OfficeAssignment OfficeAssignment { get; set; }
Создание сущности OfficeAssignment
Создайте Models/OfficeAssignment.cs
, используя следующий код:
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; }
}
}
Атрибут Key
Атрибут [Key]
используется для идентификации свойства в качестве первичного ключа (PK), когда имя свойства отличается от classnameID или ID.
Между сущностями Instructor
и OfficeAssignment
действует связь один к нулю или к одному. Назначение кабинета существует только в связи с преподавателем, которому оно назначено. Первичный ключ OfficeAssignment
также является внешним ключом (FK) для сущности Instructor
. EF Core не может автоматически распознаваться InstructorID
как PK OfficeAssignment
из-за следующих действий:
InstructorID
не соблюдает соглашение об именовании ID или classnameID.
Таким образом, атрибут Key
используется для определения InstructorID
в качестве первичного ключа:
[Key]
public int InstructorID { get; set; }
По умолчанию EF Core ключ обрабатывается как небазовый, так как столбец предназначен для идентификации связи.
Свойство навигации Instructor
Свойство навигации OfficeAssignment
для сущности Instructor
допускает значение null по следующим причинам:
- Ссылочные типы (например, классы) допускают значение null.
- Преподавателю может быть не назначен кабинет.
Сущность OfficeAssignment
имеет свойство навигации Instructor
, не допускающее значение null, по следующим причинам:
InstructorID
не допускает значение null.- Назначение кабинета не может существовать без преподавателя.
Когда сущность Instructor
имеет связанную сущность OfficeAssignment
, каждая из них имеет ссылку на другую в своем свойстве навигации.
Атрибут [Required]
можно применить к свойству навигации Instructor
:
[Required]
public Instructor Instructor { get; set; }
Предыдущий код указывает, что должен существовать связанный преподаватель. Этот код не нужен, так как внешний ключ InstructorID
(который также является первичным) не допускает значение null.
Изменение сущности Course
Обновите Models/Course.cs
, включив в него следующий код.
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; }
}
}
Сущность Course
имеет свойство внешнего ключа (FK) DepartmentID
. DepartmentID
указывает на связанную сущность Department
. Сущность Course
имеет свойство навигации Department
.
EF Core Не требуется свойство FK для модели данных, если модель имеет свойство навигации для связанной сущности.
EF Core автоматически создает FK в базе данных, где бы они ни находились. EF Core создает теневые свойства для автоматически созданных FK. Наличие внешнего ключа в модели данных позволяет сделать обновления проще и эффективнее. Например, рассмотрим модель, где свойство внешнего ключа DepartmentID
не включено. При получении сущности курса для редактирования:
- сущность
Department
имеет значение null, если она не загружена явно; - для обновления сущности курса нужно сначала получить сущность
Department
.
Если свойство внешнего ключа DepartmentID
включено в модель данных, получать сущность Department
перед обновлением не нужно.
Атрибут DatabaseGenerated
Атрибут [DatabaseGenerated(DatabaseGeneratedOption.None)]
указывает, что первичный ключ предоставляется приложением, а не создается базой данных.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
По умолчанию предполагается, EF Core что значения PK создаются базой данных. Обычно лучше всего использовать значения первичного ключа, созданные базой данных. Для сущностей Course
пользователь указывает первичный ключ. Например, номер курса, такой как серия 1000 для кафедры математики и серия 2000 для кафедры английского языка.
Атрибут DatabaseGenerated
также может использоваться для создания значений по умолчанию. Например, база данных может автоматически создать поле даты для записи даты, когда строка была создана или изменена. Дополнительные сведения см. в разделе Созданные свойства.
Свойства внешнего ключа и навигации
Свойства внешнего ключа (FK) и свойства навигации в сущности Course
отражают следующие связи:
Курс назначается одной кафедре, поэтому имеется внешний ключ DepartmentID
и свойство навигации Department
.
public int DepartmentID { get; set; }
public Department Department { get; set; }
На курс может быть зачислено любое количество учащихся, поэтому свойство навигации Enrollments
является коллекцией:
public ICollection<Enrollment> Enrollments { get; set; }
Курс могут вести несколько преподавателей, поэтому свойство навигации CourseAssignments
является коллекцией:
public ICollection<CourseAssignment> CourseAssignments { get; set; }
CourseAssignment
описано далее.
Создание сущности Department
Создайте Models/Department.cs
, используя следующий код:
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; }
}
}
Атрибут Column
Ранее атрибут Column
использовался, чтобы изменить сопоставление имени столбца. В коде для сущности Department
атрибут Column
можно использовать, чтобы изменить сопоставление типов данных SQL. Столбец Budget
определяется с помощью типа money SQL Server в базе данных:
[Column(TypeName="money")]
public decimal Budget { get; set; }
Сопоставление столбцов обычно не требуется. EF Core Обычно выбирает соответствующий тип данных SQL Server на основе типа СРЕДЫ CLR для свойства. Тип decimal
среды CLR сопоставляется с типом decimal
SQL Server. Budget
используется для денежных единиц, хотя для этого лучше подходит тип данных money.
Свойства внешнего ключа и навигации
Свойства внешнего ключа и навигации отражают следующие связи:
- Кафедра может иметь или не иметь администратора.
- Администратор всегда является преподавателем. Поэтому свойство
InstructorID
включается в качестве внешнего ключа в сущностьInstructor
.
Свойство навигации называется Administrator
, но содержит сущность Instructor
:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
Вопросительный знак (?) в приведенном выше коде указывает, что свойство допускает значение null.
Кафедра может иметь несколько курсов, поэтому доступно свойство навигации Courses:
public ICollection<Course> Courses { get; set; }
Примечание. По соглашению EF Core включает каскадное удаление для ненулевого FK и для связей "многие ко многим". Каскадное удаление может привести к циклическим правилам каскадного удаления. Такие правила вызывают исключение при добавлении миграции.
Например, если свойство Department.InstructorID
было определено как не допускающее значения NULL:
EF Core настраивает правило каскадного удаления для удаления отдела при удалении инструктора.
Удаление кафедры при удалении преподавателя не является запланированным поведением.
Следующий текучий API будет задавать правило ограничения вместо каскада.
modelBuilder.Entity<Department>() .HasOne(d => d.Administrator) .WithMany() .OnDelete(DeleteBehavior.Restrict)
Предыдущий код отключает каскадное удаление для связи кафедры и преподавателя.
Обновление сущности Enrollment
Запись зачисления обозначает один курс, который проходит один учащийся.
Обновите Models/Enrollment.cs
, включив в него следующий код.
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; }
}
}
Свойства внешнего ключа и навигации
Свойства внешнего ключа и навигации отражают следующие связи:
Запись зачисления предназначена для одного курса, поэтому доступно свойство первичного ключа CourseID
и свойство навигации Course
:
public int CourseID { get; set; }
public Course Course { get; set; }
Запись зачисления предназначена для одного учащегося, поэтому доступно свойство первичного ключа StudentID
и свойство навигации Student
:
public int StudentID { get; set; }
public Student Student { get; set; }
Связи «многие ко многим»
Между сущностями Student
и Course
действует связь многие ко многим. Сущности Enrollment
выступают в качестве таблицы соединения "многие ко многим" с полезными данными в базе данных. Фраза "с полезными данными" означает, что таблица Enrollment
содержит дополнительные данные, кроме внешних ключей для присоединяемых таблиц (в данном случае — первичный ключ и Grade
).
На следующем рисунке показано, как выглядят эти связи на схеме сущностей. (Эта схема была создана с помощью EF Power Tools для EF 6.x. Процедура ее создания не входит в этот учебник.)
Каждая линия связи имеет 1 на одном конце и звездочку (*) на другом, указывая характер один ко многим.
Если таблица Enrollment
не включала в себя сведения об оценках, ей потребуется содержать всего два внешних ключа (CourseID
и StudentID
). Таблицу соединения многие ко многим без полезных данных иногда называют чистой таблицей соединения (PJT).
Сущности Instructor
и Course
имеют связь многие ко многим с использованием чистой таблицы соединения.
Примечание. EF 6.x поддерживает неявные таблицы соединения для связей "многие ко многим", но EF Core не поддерживаются. Дополнительные сведения см. в EF Core разделе "Многие ко многим" в версии 2.0.
Сущность CourseAssignment
Создайте Models/CourseAssignment.cs
, используя следующий код:
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; }
}
}
Связь между Instructor и Courses
Связь многие ко многим между Instructor и Courses:
- Нуждается в таблице соединения, которая должна быть представлена набором сущностей.
- Является чистой таблицей соединения (таблицей без полезных данных).
Обычно для сущности соединения используется имя EntityName1EntityName2
. Например, таблицей соединения Instructor и Courses, использующей этот шаблон, является CourseInstructor
. Однако рекомендуется использовать имя, которое описывает эту связь.
Модели данных создаются простыми и разрастаются. Соединения без полезных данных (PJT) часто начинают включать их. Если в начале задать описательное имя сущности, его не нужно менять при изменениях таблицы соединения. Оптимально, если сущность соединения имеет собственное естественное имя (возможно, из одного слова) в бизнес-среде. Например, Books и Customers можно связать с сущностью соединения Ratings. Связь многие ко многим между Instructor и Courses CourseAssignment
является более предпочтительным вариантом, чем CourseInstructor
.
Составной ключ
Внешние ключи не допускают значение null. Два внешних ключа в CourseAssignment
(InstructorID
и CourseID
) совместно однозначно определяют каждую строку таблицы CourseAssignment
. CourseAssignment
не требуется выделенный первичный ключ. Свойства InstructorID
и CourseID
выступают в качестве составного первичного ключа. Единственным способом указания составных PK EF Core является использование api fluent. Следующий раздел описывает, как настроить составной первичный ключ.
Составной ключ обеспечивает следующее:
- Для одного курса допускается несколько строк.
- Для одного преподавателя допускается несколько строк.
- Несколько строк для одного преподавателя и курса недопустимы.
Сущность соединения Enrollment
определяет собственный первичный ключ, поэтому подобные дубликаты возможны. Для предотвращения подобных дубликатов:
- добавьте уникальный индекс для полей внешнего ключа или
- настройте
Enrollment
с первичным составным ключом аналогичноCourseAssignment
. Дополнительные сведения см. в разделе Индексы.
Изменение контекста базы данных
Добавьте выделенный ниже код в 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 });
}
}
}
Предыдущий код добавляет новые сущности и настраивает составной первичный ключ сущности CourseAssignment
.
Текучий API вместо атрибутов
Метод OnModelCreating
в предыдущем коде использует api fluent для настройки EF Core поведения. Этот API называется "текучим", так как часто используется для объединения серии вызовов методов в один оператор. В следующем коде показан пример текучего API:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
В этом руководстве текучий API используется только для сопоставления базы данных, которое невозможно выполнить с помощью атрибутов. Однако текучий API позволяет задать большинство правил форматирования, проверки и сопоставления, которые можно указать с помощью атрибутов.
Некоторые атрибуты, такие как MinimumLength
, невозможно применить с текучим API. MinimumLength
не изменяет схему, он лишь применяет правило проверки минимальной длины.
Некоторые разработчики предпочитают использовать текучий API монопольно, чтобы оставить свои классы сущностей "чистыми". Атрибуты и текучий API можно смешивать. Существует несколько конфигураций, которые можно реализовать только с помощью текучего API (с указанием составного первичного ключа). Существует несколько конфигураций, которые можно реализовать только с помощью атрибутов (MinimumLength
). Рекомендации по использованию текучего API и атрибутов:
- Выберите один из двух этих подходов.
- Используйте выбранный подход максимально согласованно.
Некоторые атрибуты из этого руководства используются:
- только для проверки (например,
MinimumLength
); - EF Core только конфигурация (например,
HasKey
). - Проверка и EF Core настройка (например,
[StringLength(50)]
).
Дополнительные сведения о сравнении атрибутов и текучего API см. в разделе Методы конфигурации.
Схема сущностей, показывающая связи
Ниже показана схема, создаваемая средствами EF Power Tools для завершенной модели School.
На предыдущей схеме показано следующее:
- Несколько линий связи один ко многим (1 к *).
- Линия связи один к нулю или одному (1 к 0..1) между сущностями
Instructor
иOfficeAssignment
. - Линия связи нуль или один ко многим (0..1 к *) между сущностями
Instructor
иDepartment
.
Заполнение базы данных тестовыми данными
Обновите код в 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();
}
}
}
Предыдущий код предоставляет начальные данные для новых сущностей. Основная часть кода создает объекты сущностей и загружает демонстрационные данные. Демонстрационные данные используются для проверки. См. Enrollments
и CourseAssignments
, чтобы ознакомиться с примерами заполнения данными таблиц соединения "многие ко многим".
Добавление миграции
Выполните сборку проекта.
Add-Migration ComplexDataModel
Предыдущая команда отображает предупреждение о возможной потере данных.
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'
При выполнении команды database update
возникает следующая ошибка:
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'.
Применение миграции
Теперь у вас есть база данных, и пора подумать о том, как применять к ней будущие изменения. В этом руководстве показано два подхода:
- Удаление и повторное создание базы данных
- Применение миграции к существующей базе данных. Хотя этот метод является более сложным и трудоемким, в реальной рабочей среде лучше использовать именно его. Примечание. Это необязательный раздел руководства. Вы можете выполнить удаление и повторное создание и пропустить этот раздел. Если вы хотите выполнить инструкции в этом разделе, не выполняйте удаление и повторное создание.
Удаление и повторное создание базы данных
Код в обновленном DbInitializer
предоставляет начальные данные для новых сущностей. Чтобы принудительно EF Core создать новую базу данных, удалите и обновите базу данных:
Выполните следующие команды в консоли диспетчера пакетов (PMC):
Drop-Database
Update-Database
Чтобы просмотреть справочную информацию, выполните команду Get-Help about_EntityFrameworkCore
в PMC.
Выполнить приложение. При запуске приложения выполняется метод DbInitializer.Initialize
. DbInitializer.Initialize
заполняет новую базу данных.
Откройте базу данных в SSOX:
- Если SSOX был открыт ранее, нажмите кнопку Обновить.
- Разверните узел Таблицы. Отображаются созданные таблицы.
Изучите таблицу CourseAssignment:
- Щелкните правой кнопкой мыши таблицу CourseAssignment и выберите пункт Просмотреть данные.
- Убедитесь, что таблица CourseAssignment содержит данные.
Применение миграции к существующей базе данных
Это необязательный раздел. Эти действия подходят только в том случае, если вы пропустили предыдущий раздел Удаление и повторное создание базы данных.
При выполнении миграций с существующими данными могут действовать ограничения внешнего ключа, которым существующие данные не соответствуют. Для рабочих данных нужно предпринять меры по переносу существующих данных. Этот раздел содержит пример того, как устранить нарушения ограничений внешнего ключа. Вносите эти изменения кода только после создания резервной копии. Не вносите эти изменения кода, если вы выполнили инструкции из предыдущего раздела и обновили базу данных.
Файл {timestamp}_ComplexDataModel.cs
содержит следующий код:
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
type: "int",
nullable: false,
defaultValue: 0);
Предыдущий код добавляет в таблицу Course
внешний ключ DepartmentID
, не допускающий значение null. База данных из предыдущего руководства содержит строки в Course
, поэтому эту таблицу невозможно обновить с помощью миграций.
Чтобы заставить миграцию ComplexDataModel
работать с существующими данными:
- Изменить код, чтобы присвоить новому столбцу (
DepartmentID
) значение по умолчанию. - Создайте фиктивную кафедру с именем "Temp" для использования по умолчанию.
Устранение ограничений внешнего ключа
Обновите метод Up
в классах ComplexDataModel
.
- Откройте файл
{timestamp}_ComplexDataModel.cs
. - Закомментируйте строку кода, которая добавляет столбец
DepartmentID
в таблицуCourse
.
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);
Добавьте выделенный ниже код. Новый код идет после блока .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);
После внесения описанных выше изменений существующие строки Course
будут связаны с кафедрой "Temp" после выполнения метода ComplexDataModel
Up
.
Рабочее приложение:
- включает код или сценарии, чтобы добавить строки
Department
и связанные строкиCourse
к новым строкамDepartment
; - не использует кафедру "Temp" или значение по умолчанию для
Course.DepartmentID
.
Следующее руководство посвящено связанным данным.
Дополнительные ресурсы
ASP.NET Core