Создание более сложной модели данных для приложения MVC ASP.NET (4 из 10)

Том Дайкстра

Пример веб-приложения Contoso University демонстрирует создание ASP.NET приложений MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии руководств см. в первом руководстве серии.

Примечание

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

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

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

School_class_diagram

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

В этом разделе вы узнаете, как настроить модель данных с помощью атрибутов, которые указывают правила форматирования, проверки и сопоставления базы данных. Затем в нескольких из следующих разделов вы создадите полную 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 StudentID { 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)]
public DateTime EnrollmentDate { get; set; }

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

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

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

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

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

Students_index_page_with_formatted_date

The StringLengthAttribute

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

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { 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 MaxLengthOnNames создает файл с именем <timeStamp>_MaxLengthOnNames.cs. Этот файл содержит код, который обновит базу данных в соответствии с текущей моделью данных. Метка времени, добавляемая к имени файла миграций, используется Entity Framework для упорядочения миграций. После создания нескольких миграций, удаления базы данных или развертывания проекта с помощью миграций все миграции применяются в том порядке, в котором они были созданы.

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

Ошибка val на стороне клиента

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

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

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

Добавьте в свойство оператор 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 StudentID { 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

В Обозреватель сервера (Обозреватель базы данных, если вы используете Express для Интернета), дважды щелкните таблицу Student.

Снимок экрана: таблица Student в серверном Обозреватель.

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

Снимок экрана: таблица Student в серверном Обозреватель. Строка

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

Примечание

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

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

Instructor_entity

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

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

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int InstructorID { 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; }

        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

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

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

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

Атрибуты свойства указывают, LastName что это обязательное поле, что подпись текстового поля должно быть "Фамилия" (вместо имени свойства, которое будет "Фамилия" без пробела) и что значение не может быть длиннее 50 символов.

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

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

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

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

public class Instructor
{
   public int InstructorID { 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; }

   public string FullName
   {
      get { return LastName + ", " + FirstMidName; }
   }

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

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

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

public string FullName
{
    get { return LastName + ", " + FirstMidName; }
}

Свойства навигации Courses и OfficeAssignment

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

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

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

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

OfficeAssignment_entity

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

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

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        [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 fluent связей или заметок к данным.

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

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

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

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

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

Course_entity

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

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

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

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

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

      [Display(Name = "Department")]
      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 внешнего ключа, указывающее на связанную 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

Department_entity

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

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

namespace ContosoUniversity.Models
{
   public class Department
   {
      public int DepartmentID { get; set; }

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

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

      [DataType(DataType.Date)]
      public DateTime StartDate { get; set; }

      [Display(Name = "Administrator")]
      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 больше подходит для этого.

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

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

  • Кафедра может иметь или не иметь администратора, и администратор всегда является преподавателем. Таким образом 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);

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

Student_entity

В Файле Models\Student.cs замените код, добавленный ранее, приведенным ниже. Изменения выделены.

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

namespace ContosoUniversity.Models
{
   public class Student
   {
      public int StudentID { get; set; }

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

      [StringLength(50, MinimumLength = 1, 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)]
      [Display(Name = "Enrollment Date")]
      public DateTime EnrollmentDate { get; set; }

      public string FullName
      {
         get { return LastName + ", " + FirstMidName; }
      }

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

Сущность регистрации

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

Студенческая Course_many в many_relationship

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

В этом руководстве вы будете использовать текучий 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 System;
   using System.Collections.Generic;
   using System.Data.Entity;
   using System.Data.Entity.Migrations;
   using System.Linq;
   using ContosoUniversity.Models;
   using ContosoUniversity.DAL;

   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").InstructorID },
                new Department { Name = "Mathematics", Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").InstructorID },
                new Department { Name = "Engineering", Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").InstructorID },
                new Department { Name = "Economics",   Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").InstructorID }
            };
         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").InstructorID, 
                    Location = "Smith 17" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Harui").InstructorID, 
                    Location = "Gowan 27" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").InstructorID, 
                    Location = "Thompson 304" },
            };
         officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.Location, 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").StudentID, 
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, 
                    Grade = Grade.A 
                },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, 
                    Grade = Grade.C 
                 },                            
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, 
                    Grade = Grade.B
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").StudentID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").StudentID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B         
                 },
                new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Barzdukas").StudentID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Li").StudentID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Justice").StudentID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B         
                 }
            };

         foreach (Enrollment e in enrollments)
         {
            var enrollmentInDataBase = context.Enrollments.Where(
                s =>
                     s.Student.StudentID == 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,
       Department = departments.Single( s => s.Name == "Engineering"),
       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 команду :

PM> add-Migration Chap4

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

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

Измените < файл timestamp>_Chap4.cs и внесите следующие изменения в код (вы добавите инструкцию SQL и измените инструкцию AddColumn ):

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));
    AddForeignKey("dbo.Course", "DepartmentID", "dbo.Department", "DepartmentID", cascadeDelete: true);
    CreateIndex("dbo.Course", "DepartmentID");
}

public override void Down()
{

(Убедитесь, что вы закомментируете или удалите существующую AddColumn строку при добавлении новой. Иначе при вводе команды появится сообщение об ошибке update-database .)

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

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

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

Примечание

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

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

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

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

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

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

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

Table_data_in_CourseInstructor_table

Сводка

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

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