Руководство. Создание более сложной модели данных для приложения MVC ASP.NET

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

По завершении работы классы сущностей сформируют готовую модель данных, приведенную на следующем рисунке:

School_class_diagram

Изучив это руководство, вы:

  • Настройка модели данных
  • Обновление сущности Student
  • Создание сущности Instructor
  • Создание сущности OfficeAssignment
  • Изменение сущности Course
  • Создание сущности Department
  • Изменение сущности Enrollment
  • Добавление кода в контекст базы данных
  • Начальное заполнение базы данных тестовыми данными
  • Добавление миграции
  • Обновление базы данных

Предварительные требования

Настройка модели данных

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

Атрибут DataType

Сейчас для дат зачисления студентов учащихся все веб-страницы отображают время и дату, хотя для этого поля достаточно одной даты. Используя атрибуты заметок к данным, вы можете внести в код одно изменение, позволяющее исправить формат отображения в каждом представлении, где отображаются эти данные. Чтобы рассмотреть соответствующий пример, вы добавите атрибут в свойство EnrollmentDate класса Student.

В Файле Models\Student.cs добавьте using оператор для System.ComponentModel.DataAnnotations пространства имен и добавьте DataType атрибуты и DisplayFormat в EnrollmentDate свойство , как показано в следующем примере:

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

Атрибут DataType используется для указания типа данных, который более специфичен, чем встроенный тип базы данных. В этом случае требуется отслеживать только дату, а не дату и время. Перечисление DataType предоставляет множество типов данных, таких как Date, Time, PhoneNumber, Currency, EmailAddress и многое другое. Атрибут DataType также обеспечивает автоматическое предоставление функций для определенных типов в приложении. Например, mailto: можно создать ссылку для DataType.EmailAddress, а также селектор даты для DataType.Date в браузерах, поддерживающих HTML5. Атрибуты DataType выдают атрибуты HTML 5 data- (произносится тире данных), которые могут понять браузеры HTML 5. Атрибуты DataType не обеспечивают проверку.

DataType.Date не задает формат отображаемой даты. По умолчанию поле данных отображается в соответствии с форматами по умолчанию на основе CultureInfo сервера.

С помощью атрибута DisplayFormat можно явно указать формат даты:

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

Параметр ApplyFormatInEditMode указывает, что указанное форматирование также должно применяться при отображении значения в текстовом поле для редактирования. (Для некоторых полей это может не потребоваться, например для значений валют, символ валюты в текстовом поле не требуется для редактирования.)

Атрибут DisplayFormat можно использовать сам по себе, но обычно рекомендуется также использовать атрибут DataType . Атрибут DataType передает семантику данных, а не способ их отображения на экране, и предоставляет следующие преимущества, которые вы не получаете с помощью DisplayFormat:

  • Поддержка функций HTML5 в браузере (отображение элемента управления календарем, соответствующего языковому стандарту символа валюты, ссылок электронной почты, проверки на стороне клиента и т. д.).
  • По умолчанию браузер будет отображать данные в правильном формате в зависимости от вашего языкового стандарта.
  • Атрибут DataType позволяет MVC выбрать правильный шаблон поля для отрисовки данных ( DisplayFormat использует шаблон строки). Дополнительные сведения см. в статье Шаблоны ASP.NET MVC 2 Брэда Уилсона. (Хотя эта статья написана для MVC 2, она по-прежнему относится к текущей версии ASP.NET MVC.)

Если вы используете DataType атрибут с полем даты, необходимо также указать DisplayFormat атрибут , чтобы убедиться, что поле правильно отображается в браузерах Chrome. Дополнительные сведения см. в этом потоке StackOverflow.

Дополнительные сведения о том, как обрабатывать другие форматы дат в MVC, см. в статье Введение в MVC 5. Изучение методов редактирования и редактирования представления и поиск на странице "интернационализация".

Запустите страницу Индекса учащихся еще раз и обратите внимание, что время больше не отображается для дат регистрации. То же самое относится и к любому представлению, в котором используется Student модель.

Students_index_page_with_formatted_date

Атрибут StringLengthAttribute

С помощью атрибутов также можно указать правила проверки данных и сообщения об ошибках проверки. Атрибут StringLength задает максимальную длину в базе данных и обеспечивает проверку на стороне клиента и сервера для ASP.NET MVC. В этом атрибуте также можно указать минимальную длину строки, но это минимальное значение не влияет на схему базы данных.

Предположим, вы хотите сделать так, чтобы пользователи не вводили больше 50 символов для имени. Чтобы добавить это ограничение, добавьте атрибуты StringLength в LastName свойства и FirstMidName , как показано в следующем примере:

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

Атрибут StringLength не запрещает пользователю вводить пробелы для имени. Для применения ограничений к входным данным можно использовать атрибут RegularExpression . Например, следующий код требует, чтобы первый символ был прописным, а остальные символы — в алфавитном порядке:

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

Атрибут MaxLength предоставляет функции, аналогичные атрибуту StringLength , но не обеспечивает проверку на стороне клиента.

Запустите приложение и откройте вкладку Учащиеся . Возникает следующая ошибка:

Модель, которая поддерживает контекст SchoolContext, изменилась с момента создания базы данных. Попробуйте обновить базу данных с помощью Code First Migrations (https://go.microsoft.com/fwlink/?LinkId=238269).

Модель базы данных изменилась таким образом, что требуется изменить схему базы данных, и Entity Framework обнаружила это. Вы будете использовать миграции для обновления схемы без потери данных, добавленных в базу данных с помощью пользовательского интерфейса. Если вы изменили данные, созданные Seed с помощью метода , они будут возвращены в исходное состояние из-за метода AddOrUpdate , который используется в методе Seed . (AddOrUpdate эквивалентен операции upsert из терминологии базы данных.)

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

add-migration MaxLengthOnNames
update-database

Команда add-migration создает файл с именем <timeStamp>_MaxLengthOnNames.cs. Он содержит в методе Up код, который обновит базу данных в соответствии с текущей моделью данных. Команда update-database запустила этот код.

Метка времени, добавляемая к имени файла миграций, используется Entity Framework для упорядочения миграций. Перед выполнением update-database команды можно создать несколько миграций, а затем все миграции применяются в том порядке, в котором они были созданы.

Запустите страницу Создание и введите имя длиной более 50 символов. При нажатии кнопки Создать проверка на стороне клиента отображает сообщение об ошибке: Поле LastName должно быть строкой с максимальной длиной 50.

Атрибут столбца

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

Атрибут Column указывает, что при создании базы данных столбец таблицы Student, сопоставляемый со свойством FirstMidName, будет называться FirstName. Другими словами, когда ваш код ссылается на Student.FirstMidName, данные будут браться из столбца FirstName таблицы Student или обновляться в нем. Если не указать имена столбцов, они получают то же имя, что и имя свойства.

В файле Student.cs добавьте using оператор для System.ComponentModel.DataAnnotations.Schema и атрибут имени столбца к свойству FirstMidName , как показано в следующем выделенном коде:

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]       
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

Добавление атрибута Column изменяет модель, резервную копию SchoolContext, поэтому она не будет соответствовать базе данных. Введите следующие команды в PMC, чтобы создать еще одну миграцию:

add-migration ColumnFirstName
update-database

В Обозреватель сервера откройте конструктор таблиц Student, дважды щелкнув таблицу Student.

На следующем рисунке показано имя исходного столбца до применения первых двух миграций. В дополнение к изменению имени столбца с FirstMidName на FirstName, два столбца имен изменились с MAX длины до 50 символов.

Два снимка экрана: различия в имени и типе данных двух таблиц Student.

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

Примечание

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

Обновление сущности 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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

Обязательный атрибут

Атрибут Required делает свойства имен обязательными полями. не Required attribute требуется для таких типов значений, как DateTime, int, double и float. Типы значений не могут быть присвоены значения NULL, поэтому они по своей природе обрабатываются как обязательные поля.

Для применения MinimumLength нужно использовать атрибут Required с MinimumLength.

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

MinimumLength и Required разрешают использовать пробелы при проверке. RegularExpression Используйте атрибут для полного управления строкой.

Атрибут отображения

Атрибут Display указывает, что заголовки для текстовых полей должны иметь вид "First Name" (Имя), "Last Name" (Фамилия), "Full Name" (Полное имя) и "Enrollment Date" (Дата зачисления) вместо имени свойства в каждом экземпляре (в котором не используется пробел для разделения слов).

Вычисляемое свойство FullName

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

Создание сущности Instructor

Создайте Файл Models\Instructor.cs, заменив код шаблона следующим кодом:

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

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

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

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

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

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

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

Обратите внимание, что некоторые свойства являются одинаковыми в сущностях Student и Instructor. В руководстве по реализации наследования далее в этой серии вы выполните рефакторинг данного кода, чтобы устранить избыточность.

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

public class Instructor
{
   public int ID { get; set; }

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

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

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

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

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

Свойства навигации "Курсы" и OfficeAssignment

Courses и OfficeAssignment — это свойства навигации. Как было сказано ранее, они обычно определяются как виртуальные , чтобы они могли воспользоваться преимуществами функции Entity Framework, называемой отложенной загрузкой. Кроме того, если свойство навигации может содержать несколько сущностей, его тип должен реализовывать интерфейс ICollection<T> . Например , IList<T> имеет право, но не IEnumerable<T> , так как IEnumerable<T> не реализует Add.

Преподаватель может преподавать любое количество курсов, поэтому Courses определяется как коллекция сущностей Course .

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

Наши бизнес-правила утверждают, что преподаватель может иметь не более одного офиса, поэтому OfficeAssignment определяется как единая OfficeAssignment сущность (что может быть null , если офис не назначен).

public virtual OfficeAssignment OfficeAssignment { get; set; }

Создание сущности OfficeAssignment

Создайте Файл Models\OfficeAssignment.cs со следующим кодом:

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

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        [ForeignKey("Instructor")]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public virtual Instructor Instructor { get; set; }
    }
}

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

Атрибут ключа

Между сущностями Instructor и OfficeAssignment действует связь "один к нулю или к одному". Назначение кабинета существует только в связи с преподавателем, которому оно назначено, поэтому его первичный ключ также является внешним ключом для сущности Instructor. Но Платформа Entity Framework не может автоматически распознать InstructorID как первичный ключ этой сущности, так как ее имя не соответствует соглашению ID об именовании или имениID класса. Таким образом, атрибут Key используется для определения ее в качестве ключа:

[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }

Атрибут можно также использовать, Key если сущность имеет собственный первичный ключ, но требуется присвоить свойству имя, отличное classnameID от или ID. По умолчанию EF обрабатывает ключ как не созданный базой данных, так как столбец предназначен для идентифицируемой связи.

Атрибут ForeignKey

Если между двумя сущностями (например, между и ) существует отношение "один к нулю или один" или отношение "один к одному" (например, между OfficeAssignment и Instructor), EF не может определить, какой конец отношения является основным, а какой — зависимым. Связи "один к одному" имеют свойство навигации по ссылке в каждом классе к другому классу. Атрибут ForeignKey можно применить к зависимому классу, чтобы установить связь. Если опустить атрибут ForeignKey, при попытке создать миграцию вы получите следующую ошибку:

Не удалось определить основной конец связи между типами ContosoUniversity.Models.OfficeAssignment и ContosoUniversity.Models.Instructor. Основной конец этой связи должен быть явно настроен с помощью либо текучих API связей, либо примечаний к данным.

Далее в этом руководстве вы узнаете, как настроить эту связь с помощью беглого API.

Свойство навигации instructor

Сущность Instructor имеет свойство навигации, допускающее OfficeAssignment значение NULL (так как у преподавателя может не быть задания офиса), а OfficeAssignment у сущности есть свойство навигации, не допускающее Instructor значения NULL (так как задание офиса не может существовать без преподавателя — InstructorID не допускает значения NULL). Instructor Если сущность имеет связанную OfficeAssignment сущность, каждая сущность будет иметь ссылку на другую в своем свойстве навигации.

Можно поместить [Required] атрибут в свойство навигации Instructor, чтобы указать, что должен быть связанный инструктор, но это не нужно делать, так как внешний ключ InstructorID (который также является ключом для этой таблицы) не допускает значения NULL.

Изменение сущности Course

В файле Models\Course.cs замените ранее добавленный код следующим кодом:

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

namespace ContosoUniversity.Models
{
   public class Course
   {
      [DatabaseGenerated(DatabaseGeneratedOption.None)]
      [Display(Name = "Number")]
      public int CourseID { get; set; }

      [StringLength(50, MinimumLength = 3)]
      public string Title { get; set; }

      [Range(0, 5)]
      public int Credits { get; set; }

      public int DepartmentID { get; set; }

      public virtual Department Department { get; set; }
      public virtual ICollection<Enrollment> Enrollments { get; set; }
      public virtual ICollection<Instructor> Instructors { get; set; }
   }
}

Сущность курса имеет свойство DepartmentID внешнего ключа, указывающее на связанную DepartmentDepartment сущность, и свойство навигации. Платформа Entity Framework не требует добавлять свойство внешнего ключа в модель данных при наличии свойства навигации для связанной сущности. EF автоматически создает внешние ключи в базе данных везде, где они необходимы. Однако наличие внешнего ключа в модели данных позволяет сделать обновления проще и эффективнее. Например, при выборе сущности курса для изменения сущность имеет значение NULL, Department если вы не загружаете ее, поэтому при обновлении сущности курса необходимо сначала получить Department сущность. Если свойство внешнего ключа DepartmentID включено в модель данных, получать сущность Department перед обновлением не нужно.

Атрибут DatabaseGenerated

Атрибут DatabaseGenerated с параметром None в свойстве CourseID указывает, что значения первичного ключа предоставляются пользователем, а не создаются базой данных.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

По умолчанию Entity Framework предполагает, что значения первичного ключа создаются базой данных. Именно это и требуется для большинства сценариев. Однако для сущностей Course вы будете использовать определяемый пользователем номер курса, например серия 1000 для одной кафедры, серия 2000 для другой и так далее.

Внешний ключ и свойства навигации

Свойства внешнего ключа и свойства навигации в сущности Course отражают следующие связи:

  • Курс назначается одной кафедре, поэтому по указанным выше причинам имеется внешний ключ DepartmentID и свойство навигации Department.

    public int DepartmentID { get; set; }
    public virtual Department Department { get; set; }
    
  • На курс может быть зачислено любое количество учащихся, поэтому свойство навигации Enrollments является коллекцией:

    public virtual ICollection<Enrollment> Enrollments { get; set; }
    
  • Курс могут вести несколько преподавателей, поэтому свойство навигации Instructors является коллекцией:

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

Атрибут столбца

Ранее вы использовали атрибут Column для изменения сопоставления имен столбцов. В коде для сущности Department атрибут используется для изменения сопоставления типов данных SQL, Column чтобы столбец был определен с помощью типа SQL Server money в базе данных:

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

Сопоставление столбцов обычно не требуется, так как Entity Framework обычно выбирает подходящий SQL Server тип данных на основе типа CLR, определяемого для свойства . Тип decimal среды CLR сопоставляется с типом decimal SQL Server. Но в этом случае вы знаете, что столбец будет хранить денежные суммы, и тип данных money является более подходящим для этого. Дополнительные сведения о типах данных CLR и их сопоставлении с SQL Server типами данных см. в статье SqlClient for Entity FrameworkTypes.

Внешний ключ и свойства навигации

Свойства внешнего ключа и навигации отражают следующие связи:

  • Кафедра может иметь или не иметь администратора, и администратор всегда является преподавателем. Таким образом InstructorID , свойство включается в качестве внешнего ключа сущности Instructor , а знак вопроса добавляется после int обозначения типа, чтобы пометить свойство как допускающее значение NULL. Свойство навигации называется Administrator , но содержит Instructor сущность:

    public int? InstructorID { get; set; }
    public virtual Instructor Administrator { get; set; }
    
  • В отделе может быть много курсов, поэтому есть Courses свойство навигации:

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

    Примечание

    По соглашению Entity Framework разрешает каскадное удаление для внешних ключей, не допускающих значение null, и связей многие ко многим. Это может привести к циклическим правилам каскадного удаления, которые вызывают исключение при попытке добавить миграцию. Например, если вы не определили Department.InstructorID свойство как допускающее значение NULL, вы получите следующее сообщение об исключении: "Ссылочная связь приведет к циклической ссылке, которая не допускается". Если бизнес-правила требуют InstructorID , чтобы свойство не допускало значение NULL, необходимо использовать следующую инструкцию текучих API, чтобы отключить каскадное удаление для связи:

modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);

Изменение сущности Enrollment

В файле Models\Enrollment.cs замените добавленный ранее код следующим кодом.

using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public virtual Course Course { get; set; }
        public virtual Student Student { get; set; }
    }
}

Внешний ключ и свойства навигации

Свойства внешнего ключа и навигации отражают следующие связи:

  • Запись зачисления предназначена для одного курса, поэтому доступно свойство первичного ключа CourseID и свойство навигации Course:

    public int CourseID { get; set; }
    public virtual Course Course { get; set; }
    
  • Запись зачисления предназначена для одного учащегося, поэтому доступно свойство первичного ключа StudentID и свойство навигации Student:

    public int StudentID { get; set; }
    public virtual Student Student { get; set; }
    

Связи многие ко многим

Между сущностями Student и Course имеется связь "многие ко многим", а сущность Enrollmentвыступает в качестве таблицы соединения "многие ко многим" с полезными данными в базе данных. Это означает, что Enrollment таблица содержит дополнительные данные помимо внешних ключей для объединенных таблиц (в данном случае первичный ключ и Grade свойство).

На следующем рисунке показано, как выглядят эти связи на схеме сущностей. (Эта схема была создана с помощью Entity Framework Power Tools. Создание схемы не является частью учебника, она просто используется здесь в качестве иллюстрации.)

Many_relationship учащих Course_many ся

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

Если таблица Enrollment не включала в себя сведения об оценках, ей потребуется содержать всего два внешних ключа — CourseID и StudentID. В этом случае она будет соответствовать таблице соединения "многие ко многим" без полезных данных (или чистой таблицы соединения) в базе данных, и вам не придется создавать для нее класс модели. Сущности Instructor и Course имеют такой вид связи "многие ко многим", и, как видите, между ними нет класса сущностей:

Course_many от инструктора к many_relationship

Однако в базе данных требуется таблица соединения, как показано на следующей схеме базы данных:

Course_many от инструктора к many_relationship_tables

Entity Framework автоматически создает таблицу CourseInstructor , а вы читаете и обновляете ее косвенно, считывая и обновляя свойства навигации Instructor.Courses и Course.Instructors .

Схема связей сущностей

Ниже показана схема, создаваемая средствами Entity Framework Power Tools для завершенной модели School.

School_data_model_diagram

Помимо линий связи "многие ко многим" (* к *) и линий связи "один ко многим" (1 к *), здесь можно увидеть линию связи "один к нулю или одному" (от 1 до 0..1) между Instructor сущностями и и OfficeAssignment линию связи "ноль или один ко многим" (0..1 к *) между сущностями "Преподаватель" и "Отдел".

Добавление кода в контекст базы данных

Затем вы добавите новые сущности в SchoolContext класс и настроите некоторые из сопоставлений с помощью текучих вызовов API. API является "текучим", так как он часто используется при строковом наборе вызовов методов в одну инструкцию, как показано в следующем примере:

modelBuilder.Entity<Course>()
     .HasMany(c => c.Instructors).WithMany(i => i.Courses)
     .Map(t => t.MapLeftKey("CourseID")
         .MapRightKey("InstructorID")
         .ToTable("CourseInstructor"));

В этом руководстве вы будете использовать текучий API только для сопоставления базы данных, что невозможно сделать с атрибутами. Однако текучий API позволяет задать большинство правил форматирования, проверки и сопоставления, которые можно указать с помощью атрибутов. Некоторые атрибуты, такие как MinimumLength, невозможно применить с текучим API. Как упоминалось ранее, MinimumLength не изменяет схему, она применяет только правило проверки на стороне клиента и сервера.

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

Чтобы добавить новые сущности в модель данных и выполнить сопоставление базы данных, которое не выполнялось с помощью атрибутов, замените код в файле DAL\SchoolContext.cs следующим кодом:

using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace ContosoUniversity.DAL
{
   public class SchoolContext : DbContext
   {
      public DbSet<Course> Courses { get; set; }
      public DbSet<Department> Departments { get; set; }
      public DbSet<Enrollment> Enrollments { get; set; }
      public DbSet<Instructor> Instructors { get; set; }
      public DbSet<Student> Students { get; set; }
      public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
         modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

         modelBuilder.Entity<Course>()
             .HasMany(c => c.Instructors).WithMany(i => i.Courses)
             .Map(t => t.MapLeftKey("CourseID")
                 .MapRightKey("InstructorID")
                 .ToTable("CourseInstructor"));
      }
   }
}

Новая инструкция в методе OnModelCreating настраивает таблицу соединения "многие ко многим":

  • Для связи "многие ко многим" между Instructor сущностями и Course в коде указываются имена таблиц и столбцов для таблицы соединения. Code First может настроить связь "многие ко многим" без этого кода, но если вы не назовете его, вы получите имена по умолчанию, например InstructorInstructorID для столбца InstructorID .

    modelBuilder.Entity<Course>()
        .HasMany(c => c.Instructors).WithMany(i => i.Courses)
        .Map(t => t.MapLeftKey("CourseID")
            .MapRightKey("InstructorID")
            .ToTable("CourseInstructor"));
    

В следующем коде приведен пример того, как можно было использовать текучий API вместо атрибутов для указания связи между Instructor сущностями и OfficeAssignment :

modelBuilder.Entity<Instructor>()
    .HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);

Сведения о том, что операторы Fluent API выполняются в фоновом режиме, см. в записи блога Fluent API .

Начальное заполнение базы данных тестовыми данными

Замените код в файле Migrations\Configuration.cs следующим кодом, чтобы предоставить начальные данные для новых созданных сущностей.

namespace ContosoUniversity.Migrations
{
    using ContosoUniversity.Models;
    using ContosoUniversity.DAL;
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
    
    internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(SchoolContext context)
        {
            var students = new List<Student>
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander", 
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",    
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",     
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas", 
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",        
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",   
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",    
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",  
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

            students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
            context.SaveChanges();

            var instructors = new List<Instructor>
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie", 
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",    
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",       
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",      
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",      
                    HireDate = DateTime.Parse("2004-02-12") }
            };
            instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
            context.SaveChanges();

            var departments = new List<Department>
            {
                new Department { Name = "English",     Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };
            departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
            context.SaveChanges();

            var courses = new List<Course>
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
            };
            courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
            context.SaveChanges();

            var officeAssignments = new List<OfficeAssignment>
            {
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID, 
                    Location = "Smith 17" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID, 
                    Location = "Gowan 27" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID, 
                    Location = "Thompson 304" },
            };
            officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s));
            context.SaveChanges();

            AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
            AddOrUpdateInstructor(context, "Chemistry", "Harui");
            AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
            AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");

            AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
            AddOrUpdateInstructor(context, "Trigonometry", "Harui");
            AddOrUpdateInstructor(context, "Composition", "Abercrombie");
            AddOrUpdateInstructor(context, "Literature", "Abercrombie");

            context.SaveChanges();

            var enrollments = new List<Enrollment>
            {
                new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").ID, 
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, 
                    Grade = Grade.A 
                },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, 
                    Grade = Grade.C 
                 },                            
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, 
                    Grade = Grade.B
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B         
                 },
                new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B         
                 }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollments.Where(
                    s =>
                         s.Student.ID == e.StudentID &&
                         s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollments.Add(e);
                }
            }
            context.SaveChanges();
        }

        void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
        {
            var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
            var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
            if (inst == null)
                crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
        }
    }
}

Как вы видели в первом руководстве, большая часть этого кода просто обновляет или создает новые объекты сущностей и загружает образцы данных в свойства по мере необходимости для тестирования. Однако обратите внимание, как Course обрабатывается сущность, которая имеет связь "многие ко многим" с сущностью Instructor :

var courses = new List<Course>
{
    new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
      DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
      Instructors = new List<Instructor>() 
    },
    ...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();

При создании Course объекта свойство навигации инициализируется Instructors как пустая коллекция с помощью кода Instructors = new List<Instructor>(). Это позволяет добавлять Instructor сущности, связанные с этим Course , с помощью Instructors.Add метода . Если вы не создали пустой список, вы не сможете добавить эти связи, так как Instructors свойство будет иметь значение NULL и не будет иметь Add метода. Можно также добавить инициализацию списка в конструктор.

Добавление миграции

В PMC введите add-migration команду (пока не делайте update-database команду):

add-Migration ComplexDataModel

Если попытаться выполнить команду update-database на этом этапе (пока этого делать не нужно), возникнет следующая ошибка:

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'. (Оператор ALTER TABLE конфликтовал с ограничением FOREIGN KEY "FK_dbo.Course_dbo.Department_DepartmentID". Конфликт возник в столбце "DepartmentID" таблицы "dbo.Department" базы данных "ContosoUniversity".)

Иногда при выполнении миграции с существующими данными необходимо вставить данные заглушки в базу данных, чтобы удовлетворить ограничения внешнего ключа, и это то, что вам нужно сделать сейчас. Созданный код в методе ComplexDataModel Up добавляет в таблицу не допускающий DepartmentID значения NULL внешний Course ключ. Так как при выполнении кода в Course таблице уже есть строки, операция завершится ошибкой, AddColumn так как SQL Server не знает, какое значение следует поместить в столбец, которое не может иметь значение NULL. Поэтому необходимо изменить код, чтобы присвоить новому столбцу значение по умолчанию, и создать отдел заглушки с именем Temp, чтобы он действовал в качестве отдела по умолчанию. В результате все существующие Course строки будут связаны с отделом Temp после Up выполнения метода. Их можно связать с правильными отделами в методе Seed .

Измените < файл timestamp>_ComplexDataModel.cs, закомментируйте строку кода, которая добавляет столбец DepartmentID в таблицу Course, и добавьте следующий выделенный код (выделена также строка с комментариями):

CreateTable(
        "dbo.CourseInstructor",
        c => new
            {
                CourseID = c.Int(nullable: false),
                InstructorID = c.Int(nullable: false),
            })
        .PrimaryKey(t => new { t.CourseID, t.InstructorID })
        .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
        .ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
        .Index(t => t.CourseID)
        .Index(t => t.InstructorID);

    // Create  a department for course to point to.
    Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
    //  default value for FK points to department created above.
    AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1)); 
    //AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));

    AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));

При выполнении Seed метода он вставляет строки в таблицу Department и связывает существующие Course строки с новыми Department строками. Если вы не добавили никаких курсов в пользовательский интерфейс, вам больше не потребуется отдел Temp или значение по умолчанию в столбце Course.DepartmentID . Чтобы разрешить возможность добавления курсов с помощью приложения, необходимо также обновить Seed код метода, чтобы убедиться, что все Course строки (а не только те, которые были вставлены при предыдущих запусках Seed метода) имеют допустимые DepartmentID значения, прежде чем удалить значение по умолчанию из столбца и удалить отдел Temp.

Обновление базы данных

Завершив редактирование < файла timestamp>_ComplexDataModel.cs, введите update-database команду в PMC, чтобы выполнить миграцию.

update-database

Примечание

При переносе данных и внесении изменений в схему можно получить другие ошибки. Если вы получаете ошибки миграции, которые не удается устранить, измените имя базы данных в строке подключения или удалите базу данных. Самый простой подход — переименовать базу данных в Web.config файле. В следующем примере показано изменение имени на CU_Test:

<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;Integrated Security=SSPI;" 
      providerName="System.Data.SqlClient" />

В новой базе данных нет данных для переноса, и update-database команда с гораздо большей вероятностью завершится без ошибок. Инструкции по удалению базы данных см. в статье Удаление базы данных из Visual Studio 2012.

Если это не удается, можно попробовать повторно инициализировать базу данных, введя следующую команду в PMC:

update-database -TargetMigration:0

Откройте базу данных в Обозреватель сервера, как и ранее, и разверните узел Таблицы, чтобы увидеть, что все таблицы созданы. (Если у вас по-прежнему есть серверная Обозреватель открыта с предыдущего времени, нажмите кнопку Обновить.)

Снимок экрана: окно Обозреватель сервера. Открыта папка Таблицы в контексте учебного заведения.

Вы не создали класс модели для CourseInstructor таблицы. Как было сказано ранее, это таблица соединения для связи "многие ко многим" между Instructor сущностями и Course .

Щелкните таблицу правой CourseInstructor кнопкой мыши и выберите Показать данные таблицы , чтобы убедиться, что в ней есть данные, Instructor которые были добавлены в свойство навигации Course.Instructors .

Table_data_in_CourseInstructor_table

Получите код

Скачать завершенный проект

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

Ссылки на другие ресурсы Entity Framework можно найти в ASP.NET доступ к данным — рекомендуемые ресурсы.

Дальнейшие действия

Изучив это руководство, вы:

  • Настроена модель данных
  • Обновленная сущность Student
  • Создание сущности Instructor
  • Создание сущности OfficeAssignment
  • Изменение сущности Course
  • Создание сущности Department
  • Изменена сущность регистрации
  • Добавлен код в контекст базы данных
  • Начальное заполнение базы данных тестовыми данными
  • Добавление миграции
  • Обновление базы данных

Перейдите к следующей статье, чтобы узнать, как считывать и отображать связанные данные, которые Entity Framework загружает в свойства навигации.