Руководство. Реализация наследования — ASP.NET MVC с помощью EF Core

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

В объектно-ориентированном программировании наследование применяется для оптимизации повторного использования кода. В рамках этого учебника вы измените классы Instructor и Student таким образом, чтобы они были производными от базового класса Person, который содержит общие свойства для преподавателей и учащихся, такие как LastName. Изменения вносятся в коде, а не на веб-страницах, и автоматически отражаются в базе данных.

Изучив это руководство, вы:

  • Сопоставление наследования с базой данных
  • Создание класса Person
  • Обновление Instructor и Student
  • Добавление Person в модель
  • Создание и обновление миграций
  • Тестирование реализации

Необходимые компоненты

Сопоставление наследования с базой данных

Классы Instructor и Student в модели данных School имеют несколько идентичных свойств:

Student and Instructor classes

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

Student and Instructor classes deriving from Person class

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

Table-per-hierarchy example

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

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

Предупреждение

Таблица с типом EF Core (TPT) не поддерживается 3.x, однако она реализована в EF Core версии 5.0.

Table-per-type inheritance

Такая модель создания таблицы базы данных для каждого класса сущности называется наследованием типа одна таблица на тип (TPT).

Кроме того, можно сопоставить все не являющиеся абстрактными типы с отдельными таблицами. Все свойства класса, включая унаследованные, сопоставляются со столбцами в соответствующей таблице. Такая модель называется наследованием типа одна таблица на конкретный класс (TPC). Если реализовать наследование типа "одна таблица на конкретный класс" для показанных выше классов Person, Student и Instructor, таблицы Student и Instructor после реализации наследования будут выглядеть так же, как и до этого.

Модели наследования "одна таблица на иерархию" и "одна таблица на конкретный класс", как правило, обеспечивают более высокую производительность по сравнению с моделью "одна таблица на тип", поскольку при использовании последней могут выполняться сложные запросы на соединение.

В этом учебнике демонстрируется реализация модели наследования "одна таблица на иерархию". Платформа Entity Framework поддерживает только модель наследования "одна таблица на иерархию". Вам необходимо создать класс Person, изменить классы Instructor и Student так, чтобы они были производными от класса Person, добавить новый класс в DbContext, после чего создать миграцию.

Совет

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

Создание класса Person

В папке Models создайте файл Person.cs и замените код шаблона следующим:

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

namespace ContosoUniversity.Models
{
    public abstract class Person
    {
        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; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }
    }
}

Обновление Instructor и Student

В Instructor.cs, наследуйте класс Инструктора из класса Person и удалите поля ключей и имен. Код будет выглядеть следующим образом:

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

namespace ContosoUniversity.Models
{
    public class Instructor : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

Внесите те же изменения.Student.cs

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

namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }


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

Добавление Person в модель

Добавьте тип SchoolContext.csсущности Person в . Новые строки выделены.

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }
        public DbSet<Person> People { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
            modelBuilder.Entity<Person>().ToTable("Person");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

Это все, что требуется платформе Entity Framework для настройки наследования типа "одна таблица на иерархию". Как видно, после обновления базы данных в ней будет присутствовать таблица Person вместо таблиц Student и Instructor.

Создание и обновление миграций

Сохраните изменения и выполните сборку проекта. Затем откройте командное окно в папке проекта и введите следующую команду:

dotnet ef migrations add Inheritance

На этом этапе не выполняйте команду database update. Ее выполнение приведет к потере данных, поскольку будет удалена таблица Instructor, а таблица Student будет переименована в Person. Для сохранения существующих данных потребуется настраиваемый код.

Откройте Migrations/<timestamp>_Inheritance.cs и замените метод Up следующим кодом:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropForeignKey(
        name: "FK_Enrollment_Student_StudentID",
        table: "Enrollment");

    migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table: "Enrollment");

    migrationBuilder.RenameTable(name: "Instructor", newName: "Person");
    migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<string>(name: "Discriminator", table: "Person", nullable: false, maxLength: 128, defaultValue: "Instructor");
    migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<int>(name: "OldId", table: "Person", nullable: true);

    // Copy existing Student data into new Person table.
    migrationBuilder.Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, ID AS OldId FROM dbo.Student");
    // Fix up existing relationships to match new PK's.
    migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator = 'Student')");

    // Remove temporary key
    migrationBuilder.DropColumn(name: "OldID", table: "Person");

    migrationBuilder.DropTable(
        name: "Student");

    migrationBuilder.CreateIndex(
         name: "IX_Enrollment_StudentID",
         table: "Enrollment",
         column: "StudentID");

    migrationBuilder.AddForeignKey(
        name: "FK_Enrollment_Person_StudentID",
        table: "Enrollment",
        column: "StudentID",
        principalTable: "Person",
        principalColumn: "ID",
        onDelete: ReferentialAction.Cascade);
}

Этот код выполняет следующие задачи по обновлению базы данных:

  • Удаляет ограничения внешнего ключа и индексы, которые указывают на таблицу Student.

  • Переименовывает таблицу Instructor в Person и вносит изменения, необходимые для сохранения в ней данных из таблицы Student:

  • Добавляет допускающий значения NULL тип EnrollmentDate для учащихся.

  • Добавляет столбец дискриминатора, который указывает, представляет ли строка учащегося или преподавателя.

  • Изменяет тип HireDate на допускающий значения NULL, поскольку в строках для учащихся не будет указываться дата приема на работу.

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

  • Копирует данные из таблицы Student в таблицу Person. При этом записям учащихся назначаются новые значения первичного ключа.

  • Исправляет значения внешнего ключа, которые указывают на учащихся.

  • Повторно создает ограничения внешнего ключа и индексы, которые после этого указывают на таблицу Person.

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

Выполните команду database update.

dotnet ef database update

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

Примечание.

При изменении схемы в базе, содержащей существующие данные, возможны другие ошибки. Если вы получаете ошибки миграции, которые не удается устранить, измените имя базы данных в строке подключения или удалите базу данных. В новой базе не будет данных, которые требуется перенести, в результате чего команда обновления базы данных с большей долей вероятности завершится без ошибок. Чтобы удалить базу данных, используйте средство SSOX или выполните команду database drop в интерфейсе командной строки.

Тестирование реализации

Запустите приложение и попробуйте открыть различные страницы. Все работает так же, как и раньше.

В обозревателе объектов SQL Server разверните узел Data Connections/SchoolContext и затем Таблицы. Вы увидите, что вместо таблиц Student и Instructor появилась таблица Person. Откройте таблицу Person в конструкторе и убедитесь, что в ней представлены все столбцы, содержавшиеся в таблицах Student и Instructor.

Person table in SSOX

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

Person table in SSOX - table data

Получение кода

Скачайте или ознакомьтесь с готовым приложением.

Дополнительные ресурсы

Дополнительные сведения о наследовании на платформе Entity Framework Core см. в разделе Наследование.

Следующие шаги

Изучив это руководство, вы:

  • Сопоставление наследования с базой данных
  • Создание класса Person
  • Обновление Instructor и Student
  • Добавление Person в модель
  • Создание и обновление миграций
  • Тестирование реализации

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