Реализация наследования с помощью Entity Framework в ASP.NET приложении MVC (8 из 10)
Пример веб-приложения Университета Contoso демонстрирует создание ASP.NET приложений MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии руководств см. в первом руководстве серии.
Примечание
Если у вас возникла проблема, которую не удается устранить, скачайте завершенную главу и попробуйте воспроизвести проблему. Как правило, решение проблемы можно найти, сравнив код с готовым кодом. Сведения о некоторых распространенных ошибках и способах их устранения см. в статье Ошибки и обходные пути.
В предыдущем руководстве вы обрабатывали исключения параллелизма. В этом учебнике демонстрируется, как реализовать наследование в модели данных.
В объектно-ориентированном программировании можно использовать наследование, чтобы исключить избыточный код. В рамках этого учебника вы измените классы Instructor
и Student
таким образом, чтобы они были производными от базового класса Person
, который содержит общие свойства для преподавателей и учащихся, такие как LastName
. Изменения вносятся в коде, а не на веб-страницах, и автоматически отражаются в базе данных.
Таблица на иерархию и наследование таблицы на тип
В объектно-ориентированном программировании можно использовать наследование, чтобы упростить работу со связанными классами. Например, классы Instructor
и Student
в School
модели данных имеют несколько свойств, что приводит к избыточности кода:
Предположим, что вам требуется исключить повторяющийся код для свойств, которые являются общими для сущностей Instructor
и Student
. Можно создать базовый Person
класс, содержащий только эти общие свойства, а затем сделать сущности Instructor
и Student
наследующимися от этого базового класса, как показано на следующем рисунке:
Структура наследования может быть представлена в базе данных несколькими способами. У вас может быть таблица Person
, содержащая одновременно информацию о преподавателях и учащихся. Некоторые столбцы могут применяться только к инструкторам (HireDate
), некоторые только к учащимся (EnrollmentDate
), некоторые — к обоим (LastName
, FirstName
). Как правило, у вас есть столбец дискриминатора , указывающий, какой тип представляет каждая строка. Например, в столбце дискриминатора может указываться значение "Instructor" для преподавателей и "Student" для учащихся.
Этот шаблон создания структуры наследования сущностей из отдельной таблицы базы данных называется наследованием таблицы на иерархию (TPH).
В качестве альтернативы можно создать базу данных, которая будет иметь приближенный к структуре наследования вид. Например, можно хранить в таблице Person
только поля с именами и создать отдельные таблицы Instructor
и Student
с полями дат.
Такой шаблон создания таблицы базы данных для каждого класса сущностей называется наследованием таблицы на тип (TPT).
Шаблоны наследования TPH обычно обеспечивают более высокую производительность в Entity Framework, чем шаблоны наследования TPT, так как шаблоны TPT могут привести к сложным запросам соединения. В этом учебнике демонстрируется реализация модели наследования "одна таблица на иерархию". Для этого выполните следующие действия.
- Создайте
Person
класс и измените классыInstructor
иStudent
, чтобы они были производными отPerson
. - Добавьте код сопоставления модели с базой данных в класс контекста базы данных.
- Измените
InstructorID
иStudentID
ссылки во всем проекте наPersonID
.
Создание класса Person
Примечание. Вы не сможете скомпилировать проект после создания приведенных ниже классов, пока не обновите контроллеры, использующие эти классы.
В папке Models создайте Файл Person.cs и замените код шаблона следующим кодом:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public abstract class Person
{
[Key]
public int PersonID { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
[StringLength(50, MinimumLength = 1)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "First name must be between 2 and 50 characters.")]
public string FirstMidName { get; set; }
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
}
}
В Файле Instructor.cs наследуйте Instructor
класс от Person
класса и удалите поля ключа и имени. Код будет выглядеть следующим образом:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Instructor : Person
{
[DataType(DataType.Date)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
}
Внесите аналогичные изменения в Файл Student.cs. Класс Student
будет выглядеть следующим образом:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student : Person
{
[DataType(DataType.Date)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Добавление типа сущности Person в модель
В SchoolContext.cs добавьте DbSet
свойство для типа сущности Person
:
public DbSet<Person> People { get; set; }
Это все, что требуется платформе Entity Framework для настройки наследования типа "одна таблица на иерархию". Как вы увидите, при повторном создании базы данных вместо таблиц и Instructor
будет создана Person
таблицаStudent
.
Изменение InstructorID и StudentID на PersonID
В SchoolContext.cs в операторе сопоставления Instructor-Course измените MapRightKey("InstructorID")
на MapRightKey("PersonID")
:
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("PersonID")
.ToTable("CourseInstructor"));
Это изменение не требуется; Он просто изменяет имя столбца InstructorID в таблице соединения "многие ко многим". Если оставить имя InstructorID, приложение по-прежнему будет работать правильно. Вот готовый файл 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; }
public DbSet<Person> People { 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("PersonID")
.ToTable("CourseInstructor"));
}
}
}
Затем необходимо изменить InstructorID
на PersonID
и StudentID
на PersonID
во всем проекте , за исключением файлов миграций с метками времени в папке Migrations . Для этого можно найти и открыть только те файлы, которые необходимо изменить, а затем выполнить глобальное изменение открытых файлов. Единственный файл в папке Migrations , который следует изменить, — Migrations\Configuration.cs.
-
Важно!
Начните с закрытия всех открытых файлов в Visual Studio.
Щелкните Найти и заменить — найти все файлы в меню Правка , а затем найдите все файлы в проекте, которые содержат
InstructorID
.Откройте каждый файл в окне Результаты поиска , за исключением<файлов миграции time-stamp>_.cs в папке Migrations , дважды щелкнув одну строку для каждого файла.
Откройте диалоговое окно Замена в файлах и измените значение Look in на Все открытые документы.
Используйте диалоговое окно Заменить в файлах , чтобы изменить все
InstructorID
наPersonID.
Найдите все файлы в проекте, содержащие
StudentID
.Откройте каждый файл в окне Результаты поиска , кроме<файлов миграции time-stamp>_*.cs в папке Migrations , дважды щелкнув одну строку для каждого файла.
Откройте диалоговое окно Замена в файлах и измените значение Look in на Все открытые документы.
Используйте диалоговое окно Заменить в файлах , чтобы изменить все
StudentID
наPersonID
.Выполните построение проекта.
(Обратите внимание, что это демонстрирует недостатокclassnameID
шаблона именования первичных ключей. Если вы назвали идентификатор первичных ключей без префикса имени класса, переименование не потребуется.)
Создание и обновление файла миграций
В консоли диспетчера пакетов (PMC) введите следующую команду:
Add-Migration Inheritance
Update-Database
Выполните команду в PMC. На этом этапе команда завершится ошибкой, так как у нас есть данные, которые миграции не знают, как обрабатывать. Вы увидите следующую ошибку:
Инструкция ALTER TABLE конфликтует с ограничением FOREIGN KEY "FK_dbo. Department_dbo. Person_PersonID". Конфликт произошел в базе данных "ContosoUniversity", таблице "dbo. Person", столбец "PersonID".
Откройте раздел Миграции< timestamp>_Inheritance.cs и замените Up
метод следующим кодом:
public override void Up()
{
DropForeignKey("dbo.Department", "InstructorID", "dbo.Instructor");
DropForeignKey("dbo.OfficeAssignment", "InstructorID", "dbo.Instructor");
DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student");
DropForeignKey("dbo.CourseInstructor", "InstructorID", "dbo.Instructor");
DropIndex("dbo.Department", new[] { "InstructorID" });
DropIndex("dbo.OfficeAssignment", new[] { "InstructorID" });
DropIndex("dbo.Enrollment", new[] { "StudentID" });
DropIndex("dbo.CourseInstructor", new[] { "InstructorID" });
RenameColumn(table: "dbo.Department", name: "InstructorID", newName: "PersonID");
RenameColumn(table: "dbo.OfficeAssignment", name: "InstructorID", newName: "PersonID");
RenameColumn(table: "dbo.Enrollment", name: "StudentID", newName: "PersonID");
RenameColumn(table: "dbo.CourseInstructor", name: "InstructorID", newName: "PersonID");
CreateTable(
"dbo.Person",
c => new
{
PersonID = c.Int(nullable: false, identity: true),
LastName = c.String(maxLength: 50),
FirstName = c.String(maxLength: 50),
HireDate = c.DateTime(),
EnrollmentDate = c.DateTime(),
Discriminator = c.String(nullable: false, maxLength: 128),
OldId = c.Int(nullable: false)
})
.PrimaryKey(t => t.PersonID);
// Copy existing Student and Instructor data into new Person table.
Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, StudentId AS OldId FROM dbo.Student");
Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, HireDate, null AS EnrollmentDate, 'Instructor' AS Discriminator, InstructorId AS OldId FROM dbo.Instructor");
// Fix up existing relationships to match new PK's.
Sql("UPDATE dbo.Enrollment SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = Enrollment.PersonId AND Discriminator = 'Student')");
Sql("UPDATE dbo.Department SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = Department.PersonId AND Discriminator = 'Instructor')");
Sql("UPDATE dbo.OfficeAssignment SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = OfficeAssignment.PersonId AND Discriminator = 'Instructor')");
Sql("UPDATE dbo.CourseInstructor SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = CourseInstructor.PersonId AND Discriminator = 'Instructor')");
// Remove temporary key
DropColumn("dbo.Person", "OldId");
AddForeignKey("dbo.Department", "PersonID", "dbo.Person", "PersonID");
AddForeignKey("dbo.OfficeAssignment", "PersonID", "dbo.Person", "PersonID");
AddForeignKey("dbo.Enrollment", "PersonID", "dbo.Person", "PersonID", cascadeDelete: true);
AddForeignKey("dbo.CourseInstructor", "PersonID", "dbo.Person", "PersonID", cascadeDelete: true);
CreateIndex("dbo.Department", "PersonID");
CreateIndex("dbo.OfficeAssignment", "PersonID");
CreateIndex("dbo.Enrollment", "PersonID");
CreateIndex("dbo.CourseInstructor", "PersonID");
DropTable("dbo.Instructor");
DropTable("dbo.Student");
}
Выполните команду update-database
еще раз.
Примечание
При переносе данных и внесении изменений схемы могут возникать и другие ошибки. Если возникают ошибки миграции, которые не удается устранить, вы можете перейти к руководству, изменив строка подключения в файле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. Если вы используете этот подход, чтобы продолжить работу с руководством, пропустите шаг развертывания в конце этого руководства, так как развернутый сайт получит ту же ошибку при автоматическом выполнении миграции. Если вы хотите устранить ошибку миграции, лучшим ресурсом является один из форумов Entity Framework или StackOverflow.com.
Тестирование
Запустите сайт и попробуйте различные страницы. Все работает так же, как и раньше.
В server Обозреватель разверните узел SchoolContext, а затем — Таблицы, и вы увидите, что таблицы Student и Instructor заменены на таблицу Person. Разверните таблицу Person , и вы увидите, что она содержит все столбцы, которые были в таблицах Student и Instructor .
Щелкните таблицу Person правой кнопкой мыши и выберите команду Показать данные таблицы, чтобы просмотреть столбец дискриминатора.
На следующей схеме показана структура новой базы данных School.
Сводка
Теперь для классов , Student
и Instructor
реализовано наследование таблицы на иерархиюPerson
. Дополнительные сведения об этой и других структурах наследования см. в разделе Стратегии сопоставления наследования в блоге Мортезы Манави. В следующем руководстве вы увидите некоторые способы реализации шаблонов репозитория и единиц работы.
Ссылки на другие ресурсы Entity Framework можно найти в ASP.NET карте содержимого доступа к данным.