Руководство. Создание более сложной модели данных для приложения MVC ASP.NET
В предыдущих руководствах вы работали с простой моделью данных, состоящей из трех сущностей. В этом руководстве вы добавите дополнительные сущности и связи и настраиваете модель данных, указав правила форматирования, проверки и сопоставления баз данных. В этой статье показано два способа настройки модели данных: добавление атрибутов в классы сущностей и добавление кода в класс контекста базы данных.
По завершении работы классы сущностей сформируют готовую модель данных, приведенную на следующем рисунке:
Изучив это руководство, вы:
- Настройка модели данных
- Обновление сущности Student
- Создание сущности Instructor
- Создание сущности OfficeAssignment
- Изменение сущности Course
- Создание сущности Department
- Изменение сущности Enrollment
- Добавление кода в контекст базы данных
- Начальное заполнение базы данных тестовыми данными
- Добавление миграции
- Обновление базы данных
Необходимые компоненты
Настройка модели данных
В этом разделе вы узнаете, как настроить модель данных с помощью атрибутов, которые указывают правила форматирования, проверки и сопоставления базы данных. Затем в нескольких из следующих разделов вы создадите полную School
модель данных, добавив атрибуты в уже созданные классы и создав новые классы для оставшихся типов сущностей в модели.
Атрибут DataType
Сейчас для дат зачисления студентов учащихся все веб-страницы отображают время и дату, хотя для этого поля достаточно одной даты. Используя атрибуты заметок к данным, вы можете внести в код одно изменение, позволяющее исправить формат отображения в каждом представлении, где отображаются эти данные. Чтобы рассмотреть соответствующий пример, вы добавите атрибут в свойство EnrollmentDate
класса Student
.
В Models\Student.cs добавьте инструкцию для System.ComponentModel.DataAnnotations
пространства имен и добавьте using
DataType
и DisplayFormat
атрибуты в EnrollmentDate
свойство, как показано в следующем примере:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Атрибут DataType используется для указания типа данных, который является более конкретным, чем встроенный тип базы данных. В этом случае требуется отслеживать только дату, а не дату и время. Перечисление DataType предоставляет множество типов данных, таких как Дата, Время, PhoneNumber, Валюта, EmailAddress и многое другое. Атрибут DataType
также обеспечивает автоматическое предоставление функций для определенных типов в приложении. Например, mailto:
ссылку можно создать для DataType.EmailAddress, а селектор дат можно указать для DataType.Date в браузерах, поддерживающих HTML5. Атрибуты DataType выдают атрибуты HTML 5 данных ( выраженные дефисы данных), которые могут понять браузеры HTML 5. Атрибуты DataType не предоставляют никаких проверок.
DataType.Date
не задает формат отображаемой даты. По умолчанию поле данных отображается в соответствии с форматами по умолчанию на основе языка CultureInfo сервера.
С помощью атрибута DisplayFormat
можно явно указать формат даты:
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
Параметр ApplyFormatInEditMode
указывает, что указанное форматирование также должно применяться при отображении значения в текстовом поле для редактирования. (Возможно, для некоторых полей не требуется, например, для значений валют, может потребоваться символ валюты в текстовом поле для редактирования.)
Атрибут DisplayFormat можно использовать самостоятельно, но обычно рекомендуется использовать атрибут DataType. Атрибут DataType
передает семантику данных в отличие от способа отрисовки его на экране, и предоставляет следующие преимущества, с которыми вы не получаете:DisplayFormat
- Поддержка функций HTML5 в браузере (отображение элемента управления календарем, соответствующего языковому стандарту символа валюты, ссылок электронной почты, проверки на стороне клиента и т. д.).
- По умолчанию браузер будет отображать данные с использованием правильного формата на основе языкового стандарта.
- Атрибут DataType позволяет MVC выбрать правильный шаблон поля для отрисовки данных (DisplayFormat использует шаблон строки). Дополнительные сведения см. в разделе ASP.NET шаблонов MVC 2 Брэд Уилсона. (Хотя написана для MVC 2, эта статья по-прежнему применяется к текущей версии ASP.NET MVC.)
Если атрибут используется DataType
с полем даты, необходимо также указать DisplayFormat
атрибут, чтобы убедиться, что поле правильно отображается в браузерах Chrome. Дополнительные сведения см . в этом потоке StackOverflow.
Дополнительные сведения о том, как обрабатывать другие форматы дат в MVC, см . в руководстве по MVC 5. Изучение методов редактирования и редактирования представлений и поиск на странице "интернационализация".
Запустите страницу индекса учащихся еще раз и обратите внимание, что время больше не отображается для дат регистрации. То же самое будет верно для любого представления, использующего Student
модель.
The StringLengthAttribute
С помощью атрибутов также можно указать правила проверки данных и сообщения об ошибках проверки. Атрибут StringLength задает максимальную длину в базе данных и предоставляет проверку на стороне клиента и сервера для ASP.NET MVC. В этом атрибуте также можно указать минимальную длину строки, но это минимальное значение не влияет на схему базы данных.
Предположим, вы хотите сделать так, чтобы пользователи не вводили больше 50 символов для имени. Чтобы добавить это ограничение, добавьте атрибуты StringLength в LastName
свойства и FirstMidName
свойства, как показано в следующем примере:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Атрибут StringLength не позволит пользователю вводить пробелы для имени. Атрибут RegularExpression можно использовать для применения ограничений к входным данным. Например, следующий код требует, чтобы первый символ был верхним регистром, а остальные символы должны быть алфавитными:
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
Атрибут MaxLength предоставляет аналогичные функции атрибуту StringLength, но не предоставляет проверку на стороне клиента.
Запустите приложение и перейдите на вкладку "Учащиеся ". Вы получите следующую ошибку:
Модель, которая поддерживает контекст SchoolContext, изменилась с момента создания базы данных. Рекомендуется использовать кодовые миграции для обновления базы данных (https://go.microsoft.com/fwlink/?LinkId=238269).
Модель базы данных изменилась таким образом, чтобы требуется изменение схемы базы данных, и Entity Framework обнаружила это. Вы будете использовать миграции для обновления схемы без потери данных, добавленных в базу данных с помощью пользовательского интерфейса. Если вы изменили данные, созданные методом Seed
, это будет изменено обратно в исходное состояние из-за метода AddOrUpdate , который вы используете в методе Seed
. (AddOrUpdate эквивалентен операции upsert из терминологии базы данных.)
Введите в консоли диспетчера пакетов (PMC) следующие команды:
add-migration MaxLengthOnNames
update-database
Команда add-migration
создает файл с именем <timeStamp>_MaxLengthOnNames.cs. Он содержит в методе Up
код, который обновит базу данных в соответствии с текущей моделью данных. Команда update-database
запустила этот код.
Метка времени, предустановленная в имя файла миграции, используется Entity Framework для упорядочивания миграций. Перед выполнением update-database
команды можно создать несколько миграций, а затем все миграции применяются в том порядке, в котором они были созданы.
Запустите страницу создания и введите любое имя до 50 символов. При нажатии кнопки "Создать" на стороне клиента отображается сообщение об ошибке: поле LastName должно быть строкой с максимальной длиной 50.
Атрибут столбца
Вы также можете использовать атрибуты, чтобы управлять сопоставлением классов и свойств с базой данных. Предположим, что вы использовали имя FirstMidName
для поля имени, так как это поле также может содержать отчество. Но вам нужно, чтобы столбец базы данных назывался FirstName
, так как к этому имени привыкли пользователи, которые будут составлять нерегламентированные запросы к базе данных. Чтобы выполнить это сопоставление, можно использовать атрибут Column
.
Атрибут Column
указывает, что при создании базы данных столбец таблицы Student
, сопоставляемый со свойством FirstMidName
, будет называться FirstName
. Другими словами, когда ваш код ссылается на Student.FirstMidName
, данные будут браться из столбца FirstName
таблицы Student
или обновляться в нем. Если имена столбцов не указаны, они получают то же имя, что и имя свойства.
В файле Student.cs добавьте инструкцию для System.ComponentModel.DataAnnotations.Schema и добавьте using
атрибут имени столбца в FirstMidName
свойство, как показано в следующем выделенном коде:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Добавление атрибута Column изменяет модель, резервную копию SchoolContext, поэтому она не будет соответствовать базе данных. Введите следующие команды в PMC, чтобы создать другую миграцию:
add-migration ColumnFirstName
update-database
В обозревателе серверов откройте конструктор таблиц student , дважды щелкнув таблицу Student .
На следующем рисунке показано исходное имя столбца, как это было до применения первых двух миграций. Помимо имени столбца, изменяющегося с FirstMidName
FirstName
имени, два столбца имен изменились с MAX
длины до 50 символов.
Вы также можете внести изменения в сопоставление баз данных с помощью API Fluent, как показано далее в этом руководстве.
Примечание.
Если попытаться выполнить компиляцию до создания всех классов сущностей в следующих разделах, могут возникнуть ошибки компилятора.
Обновление сущности Student
В Models\Student.cs замените код, добавленный ранее, следующим кодом. Изменения выделены.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Обязательный атрибут
Обязательный атрибут делает свойства имени обязательными полями. Не Required attribute
требуется для таких типов значений, как DateTime, int, double и float. Типы значений не могут быть назначены пустым значением, поэтому они изначально обрабатываются как обязательные поля.
Для применения MinimumLength
нужно использовать атрибут Required
с MinimumLength
.
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength
и Required
разрешают использовать пробелы при проверке. RegularExpression
Используйте атрибут для полного управления строкой.
Атрибут отображения
Атрибут Display
указывает, что заголовки для текстовых полей должны иметь вид "First Name" (Имя), "Last Name" (Фамилия), "Full Name" (Полное имя) и "Enrollment Date" (Дата зачисления) вместо имени свойства в каждом экземпляре (в котором не используется пробел для разделения слов).
Вычисляемое свойство FullName
FullName
— это вычисляемое свойство, которое возвращает значение, созданное путем объединения двух других свойств. Поэтому он имеет только get
метод доступа, и столбец FullName
не будет создан в базе данных.
Создание сущности Instructor
Создайте models\Instructor.cs, заменив код шаблона следующим кодом:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor
{
public int ID { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
}
Обратите внимание, что некоторые свойства являются одинаковыми в сущностях Student
и Instructor
. В руководстве по реализации наследования далее в этой серии вы выполните рефакторинг данного кода, чтобы устранить избыточность.
Можно поместить несколько атрибутов в одну строку, чтобы можно было также написать класс инструктора следующим образом:
public class Instructor
{
public int ID { get; set; }
[Display(Name = "Last Name"),StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
[Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)]
public string FirstMidName { get; set; }
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
Курсы и свойства навигации OfficeAssignment
Courses
и OfficeAssignment
— это свойства навигации. Как было описано ранее, они обычно определяются как виртуальные , чтобы они могли воспользоваться функцией Entity Framework, называемой отложенной загрузкой. Кроме того, если свойство навигации может содержать несколько сущностей, его тип должен реализовать интерфейс T> ICollection<. Например , IList<T> qualifies, но не IEnumerable<T> , так как IEnumerable<T>
не реализует add.
Инструктор может научить любое количество курсов, поэтому Courses
он определяется как коллекция сущностей Course
.
public virtual ICollection<Course> Courses { get; set; }
Наши бизнес-правила определяют, что инструктор может иметь только один офис, поэтому OfficeAssignment
определяется как отдельная OfficeAssignment
сущность (которая может быть null
, если офис не назначен).
public virtual OfficeAssignment OfficeAssignment { get; set; }
Создание сущности OfficeAssignment
Создайте модели\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
если сущность имеет собственный первичный ключ, но вы хотите присвоить свойству другое значение илиID
classnameID
. По умолчанию EF обрабатывает ключ как не базы данных, так как столбец предназначен для идентификации связи.
Атрибут ForeignKey
Если между двумя сущностями (например, между OfficeAssignment
двумя Instructor
сущностями) ef не удается определить, какой конец отношения является основным, и какой конец зависит. Связи "один к одному" имеют свойство навигации ссылок в каждом классе к другому классу. Атрибут ForeignKey можно применить к зависимому классу, чтобы установить связь. Если атрибут ForeignKey опущен, при попытке создать миграцию возникает следующая ошибка:
Не удалось определить основной конец связи между типами ContosoUniversity.Models.OfficeAssignment и ContosoUniversity.Models.Instructor. Основной конец этой связи должен быть явно настроен с помощью API-интерфейса связи или заметок данных.
Далее в руководстве вы узнаете, как настроить эту связь с api fluent.
Свойство навигации инструктора
Сущность Instructor
имеет свойство навигации с значением OfficeAssignment
NULL (так как инструктор может не иметь назначения office), а OfficeAssignment
сущность имеет ненулевое свойство навигации (так как назначение офиса не может существовать без инструктора — InstructorID
не допускает значения NULLInstructor
). Instructor
Если сущность имеет связанную OfficeAssignment
сущность, каждая сущность будет иметь ссылку на другую в своем свойстве навигации.
Вы можете поместить [Required]
атрибут в свойство навигации Инструктора, чтобы указать, что должен быть связанный инструктор, но вам не нужно делать это, так как внешний ключ InstructorID (который также является ключом для этой таблицы) не допускает значение NULL.
Изменение сущности Course
В Models\Course.cs замените код, добавленный ранее, следующим кодом:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Title { get; set; }
[Range(0, 5)]
public int Credits { get; set; }
public int DepartmentID { get; set; }
public virtual Department Department { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
public virtual ICollection<Instructor> Instructors { get; set; }
}
}
Сущность курса имеет свойство DepartmentID
внешнего ключа, указывающее на связанную 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
Создайте модели\Department.cs со следующим кодом:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength=3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
public virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
}
Атрибут столбца
Ранее атрибут Column использовался для изменения сопоставления имен столбцов. В коде сущности атрибут используется для Department
изменения сопоставления типов данных SQL, Column
чтобы столбец был определен с помощью типа денег SQL Server в базе данных:
[Column(TypeName="money")]
public decimal Budget { get; set; }
Сопоставление столбцов обычно не требуется, так как Entity Framework обычно выбирает соответствующий тип данных SQL Server на основе типа СРЕДЫ CLR, определяемого для свойства. Тип decimal
среды CLR сопоставляется с типом decimal
SQL Server. Но в этом случае вы знаете, что столбец будет хранить денежные суммы, и тип данных денег лучше подходит для этого. Дополнительные сведения о типах данных СРЕДЫ CLR и их сопоставлении с типами данных SQL Server см. в разделе SqlClient для Entity FrameworkTypes.
Свойства внешнего ключа и навигации
Свойства внешнего ключа и навигации отражают следующие связи:
Кафедра может иметь или не иметь администратора, и администратор всегда является преподавателем.
InstructorID
Поэтому свойство включается в качестве внешнего ключа сущностиInstructor
, и после обозначения типа, чтобы пометить свойство как допустимое значение NULL, добавляетсяint
вопросительный знак. Свойство навигации называется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 fluent, чтобы отключить каскадное удаление связи:
modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);
Изменение сущности Enrollment
В Models\Enrollment.cs замените добавленный ранее код следующим кодом.
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }
public virtual Course Course { get; set; }
public virtual Student Student { get; set; }
}
}
Свойства внешнего ключа и навигации
Свойства внешнего ключа и навигации отражают следующие связи:
Запись зачисления предназначена для одного курса, поэтому доступно свойство первичного ключа
CourseID
и свойство навигацииCourse
:public int CourseID { get; set; } public virtual Course Course { get; set; }
Запись зачисления предназначена для одного учащегося, поэтому доступно свойство первичного ключа
StudentID
и свойство навигацииStudent
:public int StudentID { get; set; } public virtual Student Student { get; set; }
Связи «многие ко многим»
Между сущностями Student
и Course
имеется связь "многие ко многим", а сущность Enrollment
выступает в качестве таблицы соединения "многие ко многим" с полезными данными в базе данных. Это означает, что Enrollment
таблица содержит дополнительные данные, кроме внешних ключей для присоединенных таблиц (в данном случае первичный ключ и Grade
свойство).
На следующем рисунке показано, как выглядят эти связи на схеме сущностей. (Эта схема была создана с помощью Entity Framework Power Tools; создание схемы не является частью руководства, оно просто используется здесь в качестве иллюстрации.)
Каждая линия связи имеет 1 на одном конце и звездочку (*) на другом, указывая характер один ко многим.
Если таблица Enrollment
не включала в себя сведения об оценках, ей потребуется содержать всего два внешних ключа — CourseID
и StudentID
. В этом случае она будет соответствовать таблице соединения "многие ко многим" без полезных данных (или чистой таблицы соединения) в базе данных, и вам не придется создавать класс модели для него вообще. Course
Сущности Instructor
имеют такие связи "многие ко многим", и, как вы видите, между ними нет класса сущностей:
Однако в базе данных требуется таблица соединения, как показано на следующей схеме базы данных:
Entity Framework автоматически создает таблицуCourseInstructor
, и вы считываете и обновляете ее косвенно, считывая и обновляя Instructor.Courses
свойства навигации.Course.Instructors
Диаграмма отношений сущностей
Ниже показана схема, создаваемая средствами Entity Framework Power Tools для завершенной модели School.
Помимо линий связи "многие ко многим" (* и "один ко многим") и линий связи "один ко многим" (от 1 до *), можно увидеть здесь линию связи "один к нулю" (от 1 до 0.1) между сущностями и OfficeAssignment
сущностями и линией связи "ноль-один ко многим" (от 0.1 до *) между Instructor
сущностями инструктора и отдела.
Добавление кода в контекст базы данных
Затем вы добавите новые сущности в SchoolContext
класс и настройте некоторые сопоставления с помощью вызовов api fluent . API является "fluent", так как он часто используется при строке ряда вызовов методов в одну инструкцию, как показано в следующем примере:
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("InstructorID")
.ToTable("CourseInstructor"));
В этом руководстве вы будете использовать простой API только для сопоставления баз данных, которые нельзя сделать с атрибутами. Однако текучий 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 fluent, а не атрибутов для указания связи между Instructor
сущностями:OfficeAssignment
modelBuilder.Entity<Instructor>()
.HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);
Сведения о том, какие инструкции "fluent API" выполняются за кулисами, см. в записи блога API Fluent .
Начальное заполнение базы данных тестовыми данными
Замените код в файле Migrations\Configuration.cs следующим кодом, чтобы предоставить начальные данные для созданных сущностей.
namespace ContosoUniversity.Migrations
{
using ContosoUniversity.Models;
using ContosoUniversity.DAL;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(SchoolContext context)
{
var students = new List<Student>
{
new Student { FirstMidName = "Carson", LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2010-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice",
EnrollmentDate = DateTime.Parse("2011-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2005-09-01") }
};
students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
context.SaveChanges();
var instructors = new List<Instructor>
{
new Instructor { FirstMidName = "Kim", LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11") },
new Instructor { FirstMidName = "Fadi", LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06") },
new Instructor { FirstMidName = "Roger", LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01") },
new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15") },
new Instructor { FirstMidName = "Roger", LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12") }
};
instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
context.SaveChanges();
var departments = new List<Department>
{
new Department { Name = "English", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Abercrombie").ID },
new Department { Name = "Mathematics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID },
new Department { Name = "Engineering", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Harui").ID },
new Department { Name = "Economics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID }
};
departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
context.SaveChanges();
var courses = new List<Course>
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 1045, Title = "Calculus", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 2021, Title = "Composition", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 2042, Title = "Literature", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();
var officeAssignments = new List<OfficeAssignment>
{
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
Location = "Smith 17" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
Location = "Gowan 27" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
Location = "Thompson 304" },
};
officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s));
context.SaveChanges();
AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
AddOrUpdateInstructor(context, "Chemistry", "Harui");
AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");
AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
AddOrUpdateInstructor(context, "Trigonometry", "Harui");
AddOrUpdateInstructor(context, "Composition", "Abercrombie");
AddOrUpdateInstructor(context, "Literature", "Abercrombie");
context.SaveChanges();
var enrollments = new List<Enrollment>
{
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").ID,
CourseID = courses.Single(c => c.Title == "Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Justice").ID,
CourseID = courses.Single(c => c.Title == "Literature").CourseID,
Grade = Grade.B
}
};
foreach (Enrollment e in enrollments)
{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
{
var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
if (inst == null)
crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
}
}
}
Как было показано в первом руководстве, большая часть этого кода просто обновляет или создает новые объекты сущностей и загружает образцы данных в свойства, необходимые для тестирования. Однако обратите внимание, что Course
сущность, которая имеет связь "многие ко многим" с сущностью Instructor
, обрабатывается:
var courses = new List<Course>
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
Instructors = new List<Instructor>()
},
...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();
При создании Course
объекта инициализируете Instructors
свойство навигации как пустую коллекцию с помощью кода Instructors = new List<Instructor>()
. Это позволяет добавлять Instructor
сущности, связанные с этим Course
, с помощью Instructors.Add
метода. Если вы не создали пустой список, вы не сможете добавить эти связи, так как Instructors
свойство будет иметь значение NULL и не будет иметь Add
метода. Вы также можете добавить инициализацию списка в конструктор.
Добавление миграции
В PMC введите add-migration
команду (еще не выполните update-database
команду):
add-Migration ComplexDataModel
Если попытаться выполнить команду update-database
на этом этапе (пока этого делать не нужно), возникнет следующая ошибка:
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'. (Оператор ALTER TABLE конфликтовал с ограничением FOREIGN KEY "FK_dbo.Course_dbo.Department_DepartmentID". Конфликт возник в столбце "DepartmentID" таблицы "dbo.Department" базы данных "ContosoUniversity".)
Иногда при выполнении миграции с существующими данными необходимо вставить заглушки в базу данных, чтобы удовлетворить ограничения внешнего ключа, и это то, что необходимо сделать сейчас. Созданный код в методе ComplexDataModel Up
добавляет в таблицу не допускающий значения DepartmentID
NULL внешний ключ Course
. Так как при выполнении кода уже есть строки в Course
таблице, операция завершится ошибкой, AddColumn
так как SQL Server не знает, какое значение следует поместить в столбец, который не может быть null. Поэтому необходимо изменить код, чтобы дать новому столбцу значение по умолчанию и создать заглушку с именем Temp, чтобы выступать в качестве отдела по умолчанию. В результате существующие Course
строки будут связаны с отделом Temp после Up
выполнения метода. Их можно связать с правильными отделами в методе Seed
.
<Измените метку времени>_ComplexDataModel.cs файл, закомментируйте строку кода, добавляющую столбец DepartmentID в таблицу course, и добавьте следующий выделенный код (закомментированная строка также выделена):
CreateTable(
"dbo.CourseInstructor",
c => new
{
CourseID = c.Int(nullable: false),
InstructorID = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.CourseID, t.InstructorID })
.ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
.ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
.Index(t => t.CourseID)
.Index(t => t.InstructorID);
// Create a department for course to point to.
Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// default value for FK points to department created above.
AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1));
//AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));
AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));
При выполнении Seed
метода он вставляет строки в Department
таблицу и будет связывать существующие Course
строки с этими новыми Department
строками. Если вы не добавили курсы в пользовательском интерфейсе, вам больше не потребуется отдел Temp или значение по умолчанию для столбца Course.DepartmentID
. Чтобы разрешить возможность того, что кто-то мог добавить курсы с помощью приложения, вы также хотите обновить Seed
код метода, чтобы убедиться, что все Course
строки (а не только те, которые были вставлены предыдущими запусками метода), имеют допустимые DepartmentID
значения перед удалением значения по умолчанию из столбца Seed
и удаления отдела Temp.
Обновление базы данных
После завершения редактирования <метки> времени_ComplexDataModel.cs файла введите update-database
команду в PMC для выполнения миграции.
update-database
Примечание.
При переносе данных и внесении изменений схемы можно получить другие ошибки. Если вы получаете ошибки миграции, которые не удается устранить, измените имя базы данных в строке подключения или удалите базу данных. Самый простой подход — переименовать базу данных в файле Web.config . В следующем примере показано, как имя изменено на CU_Test:
<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;Integrated Security=SSPI;"
providerName="System.Data.SqlClient" />
При использовании новой базы данных нет данных для переноса, и update-database
команда гораздо чаще выполняется без ошибок. Инструкции по удалению базы данных см. в статье "Удаление базы данных" из Visual Studio 2012.
Если это не удается, попробуйте повторно инициализировать базу данных, введя следующую команду в PMC:
update-database -TargetMigration:0
Откройте базу данных в обозревателе серверов, как было сделано ранее, и разверните узел таблиц , чтобы увидеть, что все таблицы созданы. (Если у вас все еще есть Обозреватель серверов открывается с предыдущего времени, нажмите кнопку "Обновить ".)
Вы не создали класс модели для CourseInstructor
таблицы. Как описано ранее, это таблица соединения для связи "многие ко многим" между Instructor
сущностями.Course
Щелкните таблицу правой кнопкой мыши CourseInstructor
и выберите "Показать данные таблицы", чтобы убедиться, что в ней Course.Instructors
есть данные в результате Instructor
добавленных сущностей в свойство навигации.
Получение кода
Скачивание завершенного проекта
Дополнительные ресурсы
Ссылки на другие ресурсы Entity Framework можно найти в ASP.NET доступ к данным — рекомендуемые ресурсы.
Следующие шаги
Изучив это руководство, вы:
- Настройка модели данных
- Обновленная сущность Student
- Создание сущности Instructor
- Создание сущности OfficeAssignment
- Изменение сущности Course
- Создание сущности Department
- Изменена сущность регистрации
- Добавлен код в контекст базы данных
- Начальное заполнение базы данных тестовыми данными
- Добавление миграции
- Обновление базы данных
Перейдите к следующей статье, чтобы узнать, как читать и отображать связанные данные, которые Entity Framework загружает в свойства навигации.