チュートリアル: 継承を実装する - ASP.NET MVC と EF Core

前のチュートリアルでは、コンカレンシー制御の例外を処理しました。 このチュートリアルでは、データ モデルで継承を実装する方法を示します。

オブジェクト指向プログラミングでは、継承を使用してコードの再利用を容易にします。 このチュートリアルでは、InstructorStudent クラスを Person 基底クラスから派生するように変更します。この基底クラスはインストラクターと受講者の両方に共通な LastName などのプロパティを含んでいます。 どの Web ページも追加または変更しませんが、コードの一部を変更し、それらの変更はデータベースに自動的に反映されます。

このチュートリアルでは、次の作業を行いました。

  • 継承をデータベースにマップする
  • Person クラスの作成
  • Instructor と Student を更新する
  • モデルに Person を追加する
  • 移行を作成および更新する
  • 実装をテストする

必須コンポーネント

継承をデータベースにマップする

School データ モデル内の Instructor および Student クラスにはいくつかの同じプロパティがあります。

Student and Instructor classes

Instructor エンティティと Student エンティティで共有されるプロパティの冗長なコードを削除すると仮定します。 または、インストラクターと学生のどちらから名前を取得したかに関係なく、名前をフォーマットできるサービスを記述するとします。 次の図に示すように、それらの共有プロパティのみが含まれる Person 基底クラスを作成し、Instructor クラスと Studentクラスがその基底クラスから継承するようにすることができます。

Student and Instructor classes deriving from Person class

データベースでこの継承構造を表すことができるいくつかの方法があります。 受講者とインストラクターの両方に関する情報を 1 つのテーブル内に含む Person テーブルを使用できます。 一部の列 (HireDate) はインストラクターのみに適用され、一部 (EnrollmentDate) は受講者のみに適用され、一部 (LastName、FirstName) は両方に適用される可能性があります。 通常、各行がどの種類を表すかを示す識別子の列があります。 たとえば、識別子列にインストラクターを示す "Instructor" と受講者を示す "Student" がある場合があります。

Table-per-hierarchy example

1 つのデータベース テーブルからエンティティの継承構造を生成するこのパターンは、Table-per-Hierarchy (TPH) 継承と呼ばれます。

代わりに、継承構造と同じように見えるデータベースを作成することもできます。 たとえば、Person テーブルに名前フィールドのみを含め、データ フィールドが含まれる別の Instructor テーブルと Student テーブルを使用できます。

警告

Table-Per-Type (TPT) は EF Core 3.x ではサポートされていませんが、EF Core 5.0 で実装されています。

Table-per-type inheritance

このエンティティ クラスごとにデータベース テーブルを作成するパターンは、Table-Per-Type (TPT) 継承と呼ばれます。

他のオプションとして、個々のテーブルにすべての非抽象型をマップすることもできます。 継承されたプロパティを含むクラスのすべてのプロパティは、対応するテーブルの列にマップされます。 このパターンは、Table-per-Concrete Class (TPC) 継承と呼ばれます。 前に示したように、PersonStudent、および Instructor クラスの TPC 継承を実装した場合、StudentInstructor のテーブルは、継承を実装した後がその前とまったく同じに見えます。

TPC および TPH 継承パターンは、一般的に TPT 継承パターンよりも高いパフォーマンスを実現します。これは、TPT パターンの結果として複雑な結合クエリになる可能性があるためです。

このチュートリアルでは、TPH 継承の実装方法を示します。 TPH は、Entity Framework Core がサポートする唯一の継承パターンです。 実行する作業として、Person クラスを作成し、Instructor および Student クラスを Person から派生するように変更し、新しいクラスを DbContext に追加して、移行を作成します。

ヒント

次の変更を加える前に、プロジェクトのコピーを保存することを検討してください。 問題が発生して最初からやり直す必要がある場合、このチュートリアルで実行した手順を逆に実行したり、すべてのシリーズの最初に戻ったりするよりも、保存したプロジェクトから開始する方が簡単です。

Person クラスの作成

[モデル] フォルダーで、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 クラスから Instructor を派生させ、キーと名前のフィールドを削除します。 コードは次の例のように表示されます。

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 を追加する

Person エンティティ型を SchoolContext.cs に追加します。 新しい行が強調表示されます。

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 });
        }
    }
}

Table-per-Hierarchy 継承を構成するために Entity Framework に必要なのことはこれですべてです。 ご覧のように、データベースが更新されたときに、Student テーブルと Instructor テーブルの代わりに、Person テーブルがあります。

移行を作成および更新する

変更を保存し、プロジェクトをビルドします。 次に、プロジェクト フォルダーでコマンド ウィンドウを開き、次のコマンドを入力します。

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 を nul 許容にします。

  • 受講者をポイントする外部キーの更新に使用する一時的なフィールドを追加します。 Person テーブルに受講者をコピーするときに新しい主キー値を受け取ります。

  • Student テーブルから Person テーブルにデータをコピーします。 これにより、受講者に新しい主キー値が割り当てられます。

  • 受講者をポイントする外部キー値を修正します。

  • 今は Person テーブルをポイントしている外部キー制約とインデックスを再作成します

(主キーの型として整数の代わりに GUID を使用した場合は、受講者の主キー値を変更する必要はなく、これらの手順のいくつかを省略できます)。

database update コマンドを実行します。

dotnet ef database update

(実稼働システムでは、以前のデータベース バージョンに戻すために Down メソッドを使用する必要があった場合、このメソッドに対応する変更を行います。このチュートリアルでは、Down メソッドは使用しません。)

Note

データが存在するデータベースでスキーマの変更を行うと、他のエラーが発生する場合があります。 解決できない移行エラーが発生した場合は、接続文字列のデータベース名を変更するか、データベースを削除できます。 新しいデータベースには移行するデータが存在しないため、update-database コマンドがエラーなしで完了する可能性が高くなります。 データベースを削除するには、SSOX を使用するか database drop CLI コマンドを実行します。

実装をテストする

アプリを実行して、さまざまなページを試してください。 すべてが前と同じように動作します。

SQL Server オブジェクト エクスプローラーで、 [データ接続/SchoolContext] を展開し、 [テーブル] を展開すると、Student テーブルと Instructor テーブルが Person テーブルに置き換えられていることを確認できます。 Person テーブル デザイナーを開くと、Student テーブルと Student テーブルに存在していたすべての列が表示されます。

Person table in SSOX

Person テーブルを右クリックし、 [テーブル データの表示] をクリックして識別子列を表示します。

Person table in SSOX - table data

コードを取得する

完成したアプリケーションをダウンロードまたは表示する。

その他の技術情報

Entity Framework Core での継承の詳細については、「継承」を参照してください。

次の手順

このチュートリアルでは、次の作業を行いました。

  • 継承をデータベースにマップした
  • Person クラスを作成した
  • Instructor と Student を更新した
  • モデルに Person を追加した
  • 移行を作成および更新した
  • 実装をテストした

比較的高度な Entity Framework のさまざまなシナリオを処理する方法について学習するには、次のチュートリアルに進んでください。