Реализация наследования с помощью Entity Framework в ASP.NET приложении MVC (8 из 10)

Том Дайкстра (Tom Dykstra)

Пример веб-приложения Университета 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 наследующимися от этого базового класса, как показано на следующем рисунке:

Снимок экрана: классы Student и Instructor, производные от класса Person.

Структура наследования может быть представлена в базе данных несколькими способами. У вас может быть таблица Person, содержащая одновременно информацию о преподавателях и учащихся. Некоторые столбцы могут применяться только к инструкторам (HireDate), некоторые только к учащимся (EnrollmentDate), некоторые — к обоим (LastName, FirstName). Как правило, у вас есть столбец дискриминатора , указывающий, какой тип представляет каждая строка. Например, в столбце дискриминатора может указываться значение "Instructor" для преподавателей и "Student" для учащихся.

Снимок экрана: структура наследования от класса сущностей Person.

Этот шаблон создания структуры наследования сущностей из отдельной таблицы базы данных называется наследованием таблицы на иерархию (TPH).

В качестве альтернативы можно создать базу данных, которая будет иметь приближенный к структуре наследования вид. Например, можно хранить в таблице Person только поля с именами и создать отдельные таблицы Instructor и Student с полями дат.

Снимок экрана: новые таблицы базы данных Instructor и Student, производные от класса сущностей Person.

Такой шаблон создания таблицы базы данных для каждого класса сущностей называется наследованием таблицы на тип (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.

  1. Важно!

    Начните с закрытия всех открытых файлов в Visual Studio.

  2. Щелкните Найти и заменить — найти все файлы в меню Правка , а затем найдите все файлы в проекте, которые содержат InstructorID.

    Снимок экрана: окно

  3. Откройте каждый файл в окне Результаты поиска , за исключением<файлов миграции time-stamp>_.cs в папке Migrations , дважды щелкнув одну строку для каждого файла.

    Снимок экрана: окно поиска результатов. Файлы миграции меток времени вычеркнуты красным цветом.

  4. Откройте диалоговое окно Замена в файлах и измените значение Look in на Все открытые документы.

  5. Используйте диалоговое окно Заменить в файлах , чтобы изменить все InstructorID на PersonID.

    Снимок экрана: окно

  6. Найдите все файлы в проекте, содержащие StudentID.

  7. Откройте каждый файл в окне Результаты поиска , кроме<файлов миграции time-stamp>_*.cs в папке Migrations , дважды щелкнув одну строку для каждого файла.

    Снимок экрана: окно

  8. Откройте диалоговое окно Замена в файлах и измените значение Look in на Все открытые документы.

  9. Используйте диалоговое окно Заменить в файлах , чтобы изменить все StudentID на PersonID.

    Снимок экрана: окно

  10. Выполните построение проекта.

(Обратите внимание, что это демонстрирует недостаток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.

Щелкните таблицу Person правой кнопкой мыши и выберите команду Показать данные таблицы, чтобы просмотреть столбец дискриминатора.

Снимок экрана: таблица Person. Выделено имя столбца Дискриминатора.

На следующей схеме показана структура новой базы данных School.

Снимок экрана: схема базы данных School.

Сводка

Теперь для классов , Studentи Instructor реализовано наследование таблицы на иерархиюPerson. Дополнительные сведения об этой и других структурах наследования см. в разделе Стратегии сопоставления наследования в блоге Мортезы Манави. В следующем руководстве вы увидите некоторые способы реализации шаблонов репозитория и единиц работы.

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