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

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

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

School_class_diagram

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

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

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

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

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

Атрибут DataType

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

В моделс\студент.КСдобавьте оператор 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, а также можно указать селектор даты для типа данных. 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

Стрингленгсаттрибуте

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

Запустите приложение и перейдите на вкладку students (учащиеся ). Появляется следующее сообщение об ошибке:

Модель, которая является резервной моделью контекста "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.

Атрибут Column

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

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

В файле Student.CS добавьте инструкцию using для System. ComponentModel. аннотации. 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 .

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

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

Note

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

Обновить сущность Student

В моделс\студент.КСзамените код, добавленный ранее, следующим кодом. Изменения выделены.

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 attribute не требуется для таких типов значений, как DateTime, int, Double и float. Типам значений не может быть присвоено значение null, поэтому они по сути рассматриваются как обязательные поля.

Для применения Required нужно использовать атрибут MinimumLength с 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

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

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. В руководстве по реализации наследования далее в этой серии вы выполните рефакторинг данного кода, чтобы устранить избыточность.

В одной строке можно разместить несколько атрибутов, поэтому можно также написать класс 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, если Office не назначен).

public virtual OfficeAssignment OfficeAssignment { get; set; }

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

Создайте моделс\оффицеассигнмент.КС со следующим кодом:

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 сущностями существует связь "один к нулю" или "один к одному". Назначение Office существует только в отношении инструктора, которому он назначен, и поэтому его первичный ключ также является внешним ключом для Instructor сущности. Но Entity Framework не может автоматически распознать InstructorID как первичный ключ этой сущности, так как ее имя не соответствует соглашению об именовании ID или classname ID. Таким образом, атрибут Key используется для определения ее в качестве ключа:

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

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

Атрибут Фореигнкэй

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

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

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

Свойство навигации преподавателя

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

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

Изменение сущности курса

В моделс\каурсе.КСзамените код, который вы добавили ранее, следующим кодом:

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; }
   }
}

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

Атрибут Датабасеженератед

Атрибут датабасеженератед с параметром 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

Создайте моделс\департмент.КС со следующим кодом:

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

Ранее для изменения сопоставления имен столбцов использовался атрибут Column . В коде для Department сущности атрибут Column используется для изменения сопоставления типов данных SQL, чтобы столбец был определен с использованием 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 для Entity Framework.

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

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

  • Кафедра может иметь или не иметь администратора, и администратор всегда является преподавателем. Поэтому 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; }
    

    Note

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

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

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

В моделс\енроллмент.КСзамените код, который вы добавили ранее, следующим кодом.

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; создание схемы не является частью руководства, оно просто используется в качестве иллюстрации.)

Student-Course_many-many_relationship

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

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

Instructor-Course_many-many_relationship

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

Instructor-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 to *) между сущностями инструктора и отдел.

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

Далее предстоит добавить новые сущности в класс SchoolContext и настроить некоторые сопоставления с помощью вызовов API Fluent . API — это "Fluent", так как часто используется для объединения ряда вызовов методов в одну инструкцию, как показано в следующем примере:

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

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

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

Чтобы добавить новые сущности в модель данных и выполнить сопоставление базы данных, которое не выполнялось с помощью атрибутов, замените код в дал\счулконтекст.КС следующим кодом:

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"));
    

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

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

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

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

Замените код в файле 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 на этом этапе (пока этого делать не нужно), возникнет следующая ошибка:

Инструкция ALTER TABLE конфликтует с ограничением внешнего ключа "FK_dbo. Курс_dbo. Отдел_DepartmentID ". Конфликт произошел в базе данных "ContosoUniversity", таблица "dbo. Department ", столбец" DepartmentID ".

Иногда при выполнении миграции с существующими данными необходимо вставить данные заглушки в базу данных для удовлетворения ограничений внешнего ключа, и это будет сделано. Созданный код в методе Up Комплексдатамодел добавляет внешний ключ DepartmentID, не допускающий значения NULL, в таблицу Course. Поскольку в таблице Course уже есть строки при выполнении кода, операция AddColumn завершится ошибкой, так как SQL Server не знает, какое значение следует разместить в столбце, которое не может иметь значение null. Поэтому необходимо изменить код, чтобы присвоить новому столбцу значение по умолчанию, и создать отдел-заглушку с именем "Temp", который будет использоваться в качестве Отдела по умолчанию. В результате существующие Course строки будут связаны с "временным" подразделением после выполнения метода 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 значения, прежде чем удалять значение по умолчанию из столбца и удалять временный отдел.

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

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

update-database

Note

При переносе данных и внесении изменений в схему можно получить другие ошибки. Если вы получаете ошибки миграции, которые не удается устранить, измените имя базы данных в строке подключения или удалите базу данных. Самый простой подход заключается в том, чтобы переименовать базу данных в файле 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
  • Изменена сущность курса
  • Создание сущности отдела
  • Изменение сущности регистрации
  • Код добавлен в контекст базы данных
  • Начальное заполнение базы данных тестовыми данными
  • Добавление миграции
  • Обновление базы данных

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