Создание более сложной модели данных для приложения MVC ASP.NET (4 из 10)
Пример веб-приложения Contoso University демонстрирует создание ASP.NET приложений MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии руководств см. в первом руководстве серии.
Примечание
Если у вас возникла проблема, которую не удается устранить, скачайте завершенную главу и попробуйте воспроизвести проблему. Как правило, решение проблемы можно найти, сравнив код с готовым кодом. Сведения о некоторых распространенных ошибках и способах их устранения см. в статье Ошибки и обходные пути.
В предыдущих руководствах вы работали с простой моделью данных, состоящей из трех сущностей. В этом руководстве вы добавите дополнительные сущности и связи и настроите модель данных, указав правила форматирования, проверки и сопоставления баз данных. Вы увидите два способа настройки модели данных: путем добавления атрибутов в классы сущностей и путем добавления кода в класс контекста базы данных.
По завершении работы классы сущностей сформируют готовую модель данных, приведенную на следующем рисунке:
Настройка модели данных с использованием атрибутов
В этом разделе вы узнаете, как настроить модель данных с помощью атрибутов, которые указывают правила форматирования, проверки и сопоставления базы данных. Затем в нескольких из следующих разделов вы создадите полную 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
модель.
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 символов, проверка на стороне клиента сразу же отобразит сообщение об ошибке.
Атрибут столбца
Вы также можете использовать атрибуты, чтобы управлять сопоставлением классов и свойств с базой данных. Предположим, что вы использовали имя 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.
На следующем рисунке показано исходное имя столбца, как это было до применения первых двух миграций. В дополнение к изменению имени столбца с FirstMidName
на FirstName
, два столбца имен изменились с MAX
длины до 50 символов.
Вы также можете внести изменения в сопоставление базы данных с помощью API Fluent, как описано далее в этом руководстве.
Примечание
При попытке компиляции перед созданием всех этих классов сущностей могут возникнуть ошибки компилятора.
Создание сущности Instructor
Создайте Файл Models\Instructor.cs, заменив код шаблона следующим кодом:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor
{
public int 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
Создайте файл 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
В Файле 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
внешнего ключа, указывающее на связанную Department
Department
сущность, и свойство навигации. Платформа Entity Framework не требует добавлять свойство внешнего ключа в модель данных при наличии свойства навигации для связанной сущности. EF автоматически создает внешние ключи в базе данных везде, где они необходимы. Однако наличие внешнего ключа в модели данных позволяет сделать обновления проще и эффективнее. Например, при выборе сущности курса для изменения сущность имеет значение NULL, Department
если вы не загружаете ее, поэтому при обновлении сущности курса необходимо сначала получить Department
сущность. Если свойство внешнего ключа DepartmentID
включено в модель данных, получать сущность Department
перед обновлением не нужно.
Атрибут DatabaseGenerated
Атрибут DatabaseGenerated с параметром None в свойстве указывает, CourseID
что значения первичного ключа предоставляются пользователем, а не создаются базой данных.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
По умолчанию Entity Framework предполагает, что значения первичного ключа создаются базой данных. Именно это и требуется для большинства сценариев. Однако для сущностей Course
вы будете использовать определяемый пользователем номер курса, например серия 1000 для одной кафедры, серия 2000 для другой и так далее.
Внешний ключ и свойства навигации
Свойства внешнего ключа и свойства навигации в сущности Course
отражают следующие связи:
Курс назначается одной кафедре, поэтому по указанным выше причинам имеется внешний ключ
DepartmentID
и свойство навигацииDepartment
.public int DepartmentID { get; set; } public virtual Department Department { get; set; }
На курс может быть зачислено любое количество учащихся, поэтому свойство навигации
Enrollments
является коллекцией:public virtual ICollection<Enrollment> Enrollments { get; set; }
Курс могут вести несколько преподавателей, поэтому свойство навигации
Instructors
является коллекцией:public virtual ICollection<Instructor> Instructors { get; set; }
Создание сущности Department
Создайте Файл Models\Department.cs со следующим кодом:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength=3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
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
В Файле 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. Создание схемы не является частью учебника, она просто используется здесь в качестве иллюстрации.)
Каждая линия связи имеет 1 на одном конце и звездочку (*) на другом, указывая характер один ко многим.
Если таблица Enrollment
не включала в себя сведения об оценках, ей потребуется содержать всего два внешних ключа — CourseID
и StudentID
. В этом случае она будет соответствовать таблице соединения "многие ко многим" без полезных данных (или таблицы чистого соединения) в базе данных, и вам не придется создавать для нее класс модели. Сущности Instructor
и Course
имеют такое отношение "многие ко многим", и, как видите, между ними нет класса сущностей:
Однако в базе данных требуется таблица соединения, как показано на следующей схеме базы данных:
Entity Framework автоматически создает таблицу CourseInstructor
, и вы считываете и обновляете ее косвенно, считывая и обновляя свойства навигации Instructor.Courses
и Course.Instructors
.
Схема сущностей, показывающая связи
Ниже показана схема, создаваемая средствами Entity Framework Power Tools для завершенной модели School.
Помимо линий связи "многие ко многим" (* к *) и линий связи "один ко многим" (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
.
Сводка
Теперь у вас есть более сложная модель данных и соответствующая база данных. В следующем руководстве вы узнаете больше о различных способах доступа к связанным данным.
Ссылки на другие ресурсы Entity Framework можно найти в ASP.NET карте содержимого доступа к данным.
Обратная связь
https://aka.ms/ContentUserFeedback.
Ожидается в ближайшее время: в течение 2024 года мы постепенно откажемся от GitHub Issues как механизма обратной связи для контента и заменим его новой системой обратной связи. Дополнительные сведения см. в разделеОтправить и просмотреть отзыв по