Часть 5. Razor Страницы с EF Core ASP.NET Core — модель данных

Авторы: Том Дайкстра (Tom Dykstra), Джереми Ликнесс (Jeremy Likness) и Йон П. Смит (Jon P Smith)

Веб-приложение Contoso University демонстрирует создание Razor веб-приложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств см. в первом руководстве серии.

При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение и сравните его код с тем, который вы создали в процессе работы с этим руководством.

Предыдущие руководства работали с базовой моделью данных, состоящей из трех сущностей. В этом руководстве рассматриваются следующие темы:

  • Добавляются дополнительные сущности и связи.
  • Настраивается модель данных с помощью указания правил для форматирования, проверки и сопоставления базы данных.

Готовая модель данных показана на следующем рисунке:

Entity diagram

Следующая диаграмма базы данных создана с помощью средства Dataedo:

Dataedo diagram

Чтобы создать диаграмму базы данных с помощью Dataedo, выполните следующие действия:

На предыдущей диаграмме 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 (Учащийся).

Students table in SSOX before migrations

На предыдущем изображении показана схемы для таблицы 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:

    Students table in SSOX after migrations

    До применения миграции столбцы имен имели тип 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

OfficeAssignment entity

Создайте 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

Запись зачисления обозначает один курс, который проходит один учащийся.

Enrollment entity

Обновите 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. Процедура ее создания не входит в этот учебник.)

Student-Course many to many relationship

Каждая линия связи имеет 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();
        }
    }
}

Предыдущий код предоставляет начальные данные для новых сущностей. Основная часть кода создает объекты сущностей и загружает демонстрационные данные. Демонстрационные данные используются для проверки.

Применение миграции либо удаление и повторное создание

Существующую базу данных можно изменить, использую один из следующих двух подходов.

Для SQL Server применимы оба подхода. Хотя метод с применением миграции является более сложным и трудоемким, в реальной рабочей среде лучше использовать именно его.

Удаление и повторное создание базы данных

Чтобы принудительно EF Core создать новую базу данных, удалите и обновите базу данных:

  • Удалите папку Migrations.
  • Выполните следующие команды в консоли диспетчера пакетов (PMC):
Drop-Database
Add-Migration InitialCreate
Update-Database

Выполнить приложение. При запуске приложения выполняется метод DbInitializer.Initialize. DbInitializer.Initialize заполняет новую базу данных.

Откройте базу данных в SSOX.

  • Если SSOX был открыт ранее, нажмите кнопку Обновить.
  • Разверните узел Таблицы. Отображаются созданные таблицы.

Следующие шаги

В следующих двух учебниках рассказывается о том, как считывать и обновлять связанные данные.

Предыдущие руководства работали с базовой моделью данных, состоящей из трех сущностей. В этом руководстве рассматриваются следующие темы:

  • Добавляются дополнительные сущности и связи.
  • Настраивается модель данных с помощью указания правил для форматирования, проверки и сопоставления базы данных.

Готовая модель данных показана на следующем рисунке:

Entity diagram

Сущность Student

Student entity

Замените код в 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 (Учащийся).

Students table in SSOX before migrations

На предыдущем изображении показана схемы для таблицы 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:

    Students table in SSOX after migrations

    До применения миграции столбцы имен имели тип nvarchar(MAX). Теперь столбцы имен имеют тип nvarchar(50). Имя столбца изменилось с FirstMidName на FirstName.

  • Запустите приложение и перейдите на страницу Students.
  • Обратите внимание, что значения времени не вводятся и не отображаются вместе с датами.
  • Выберите Create New (Создать) и попробуйте ввести имя длиной более 50 символов.

Примечание.

В следующих разделах сборка приложения на некоторых этапах приводит к возникновению ошибок компилятора. В инструкциях указано, когда производить сборку приложения.

Сущность Instructor

Instructor entity

Создайте 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

OfficeAssignment entity

Создайте 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

Course entity

Обновите 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

Department entity

Создайте 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

Запись зачисления обозначает один курс, который проходит один учащийся.

Enrollment entity

Обновите 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. Процедура ее создания не входит в этот учебник.)

Student-Course many to many relationship

Каждая линия связи имеет 1 на одном конце и звездочку (*) на другом, указывая характер один ко многим.

Если таблица Enrollment не включала в себя сведения об оценках, ей потребуется содержать всего два внешних ключа (CourseID и StudentID). Таблицу соединения многие ко многим без полезных данных иногда называют чистой таблицей соединения (PJT).

Сущности Instructor и Course имеют связь многие ко многим с использованием чистой таблицы соединения.

Примечание. EF 6.x поддерживает неявные таблицы соединения для связей "многие ко многим", но EF Core не поддерживаются. Дополнительные сведения см. в EF Core разделе "Многие ко многим" в версии 2.0.

Сущность CourseAssignment

CourseAssignment entity

Создайте 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.

Instructor-to-Courses m:M

Обычно для сущности соединения используется имя 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.

Entity diagram

На предыдущей схеме показано следующее:

  • Несколько линий связи один ко многим (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'.

В следующем разделе вы узнаете, что делать с этой ошибкой.

Применение миграции либо удаление и повторное создание

Теперь у вас есть база данных, и пора подумать о том, как применять к ней изменения. В этом руководстве показано два подхода:

Для SQL Server применимы оба подхода. Хотя метод с применением миграции является более сложным и трудоемким, в реальной рабочей среде лучше использовать именно его.

Удаление и повторное создание базы данных

Пропустите этот раздел, если вы используете SQL Server и хотите применить миграцию в следующем разделе.

Чтобы принудительно EF Core создать новую базу данных, удалите и обновите базу данных:

  • Выполните следующие команды в консоли диспетчера пакетов (PMC):

    Drop-Database
    
  • Удалите папку Migrations, а затем выполните следующую команду:

    Add-Migration InitialCreate
    Update-Database
    

Выполнить приложение. При запуске приложения выполняется метод DbInitializer.Initialize. DbInitializer.Initialize заполняет новую базу данных.

Откройте базу данных в SSOX.

  • Если SSOX был открыт ранее, нажмите кнопку Обновить.

  • Разверните узел Таблицы. Отображаются созданные таблицы.

    Tables in SSOX

  • Изучите таблицу CourseAssignment:

    • Щелкните правой кнопкой мыши таблицу CourseAssignment и выберите пункт Просмотреть данные.
    • Убедитесь, что таблица CourseAssignment содержит данные.

    CourseAssignment data in SSOX

Применение миграции

Это необязательный раздел. Эти действия подходят только для 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 заполняет новую базу данных.

Следующие шаги

В следующих двух учебниках рассказывается о том, как считывать и обновлять связанные данные.

Предыдущие руководства работали с базовой моделью данных, состоящей из трех сущностей. В этом руководстве рассматриваются следующие темы:

  • Добавляются дополнительные сущности и связи.
  • Настраивается модель данных с помощью указания правил для форматирования, проверки и сопоставления базы данных.

Классы сущностей для готовой модели данных показаны на следующем рисунке:

Entity diagram

При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение.

Настройка модели данных с использованием атрибутов

В этом разделе модель данных настраивается с помощью атрибутов.

Атрибут 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, отображает дату без времени.

Students index page showing dates without times

Атрибут 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 (Создать), проверка на стороне клиента отображает сообщение об ошибке.

Students index page showing string length errors

В обозревателе объектов SQL Server (SSOX) откройте конструктор таблиц учащихся, дважды щелкнув таблицу Student (Учащийся).

Students table in SSOX before migrations

На предыдущем изображении показана схемы для таблицы 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:

Students table in SSOX after migrations

До применения миграции столбцы имен имели тип nvarchar(MAX). Теперь столбцы имен имеют тип nvarchar(50). Имя столбца изменилось с FirstMidName на FirstName.

Примечание.

В следующем разделе сборка приложения на некоторых этапах приводит к возникновению ошибок компилятора. В инструкциях указано, когда производить сборку приложения.

Обновление сущности Student

Student entity

Обновите 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

Instructor entity

Создайте 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

OfficeAssignment entity

Создайте 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

Course entity

Обновите 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

Department entity

Создайте 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

Запись зачисления обозначает один курс, который проходит один учащийся.

Enrollment entity

Обновите 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. Процедура ее создания не входит в этот учебник.)

Student-Course many to many relationship

Каждая линия связи имеет 1 на одном конце и звездочку (*) на другом, указывая характер один ко многим.

Если таблица Enrollment не включала в себя сведения об оценках, ей потребуется содержать всего два внешних ключа (CourseID и StudentID). Таблицу соединения многие ко многим без полезных данных иногда называют чистой таблицей соединения (PJT).

Сущности Instructor и Course имеют связь многие ко многим с использованием чистой таблицы соединения.

Примечание. EF 6.x поддерживает неявные таблицы соединения для связей "многие ко многим", но EF Core не поддерживаются. Дополнительные сведения см. в EF Core разделе "Многие ко многим" в версии 2.0.

Сущность CourseAssignment

CourseAssignment entity

Создайте 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-to-Courses m:M

Связь многие ко многим между 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.

Entity diagram

На предыдущей схеме показано следующее:

  • Несколько линий связи один ко многим (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 был открыт ранее, нажмите кнопку Обновить.
  • Разверните узел Таблицы. Отображаются созданные таблицы.

Tables in SSOX

Изучите таблицу CourseAssignment:

  • Щелкните правой кнопкой мыши таблицу CourseAssignment и выберите пункт Просмотреть данные.
  • Убедитесь, что таблица CourseAssignment содержит данные.

CourseAssignment data in SSOX

Применение миграции к существующей базе данных

Это необязательный раздел. Эти действия подходят только в том случае, если вы пропустили предыдущий раздел Удаление и повторное создание базы данных.

При выполнении миграций с существующими данными могут действовать ограничения внешнего ключа, которым существующие данные не соответствуют. Для рабочих данных нужно предпринять меры по переносу существующих данных. Этот раздел содержит пример того, как устранить нарушения ограничений внешнего ключа. Вносите эти изменения кода только после создания резервной копии. Не вносите эти изменения кода, если вы выполнили инструкции из предыдущего раздела и обновили базу данных.

Файл {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" после выполнения метода ComplexDataModelUp.

Рабочее приложение:

  • включает код или сценарии, чтобы добавить строки Department и связанные строки Course к новым строкам Department;
  • не использует кафедру "Temp" или значение по умолчанию для Course.DepartmentID.

Следующее руководство посвящено связанным данным.

Дополнительные ресурсы