チュートリアル: ASP.NET MVC 5 アプリで EF を使用して継承を実装する

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

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

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

  • 継承をデータベースにマップする方法について学びます
  • Person クラスの作成
  • Instructor と Student を更新する
  • モデルに Person を追加します
  • 移行を作成および更新します
  • 実装をテストする
  • Azure に展開する

前提条件

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

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

Student_and_Instructor_classes

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

Student_and_Instructor_classes_deriving_from_Person_class

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

Table-per-hierarchy_example

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

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

Table-per-type_inheritance

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

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

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

このチュートリアルでは、TPH 継承の実装方法を示します。 TPH は Entity Framework の既定の継承パターンであるため、Person クラスを作成し、Person から派生するように InstructorStudentクラスを変更し、新しいクラスを DbContext に追加し、移行を作成します。 (他の継承パターンを実装する方法については、MSDN Entity Framework のドキュメントで、「Table-Per-Type (TPT) の継承をマップする」と、「Table-Per-Concrete Class (TPC) の継承をマップする」をご覧ください)。

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.csStudent.cs を更新して、Person.sc から値を継承します。

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 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;
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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

モデルに Person を追加します

SchoolContext.cs で、Person エンティティ型の DbSet プロパティを追加します。

public DbSet<Person> People { get; set; }

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

移行を作成および更新します

パッケージ マネージャー コンソール (PMC) で、次のコマンドを入力します。

Add-Migration Inheritance

PMC で Update-Database コマンドを実行します。 この時点でコマンドは失敗します。これは、移行で処理方法がわからない既存のデータがあるためです。 次のようなエラー メッセージが表示されます。

オブジェクト 'dbo.Instructor' を削除できませんでした。このオブジェクトは、外部キー制約で参照されています。

Migrations<timestamp>_Inheritance.cs を開き、Up メソッドを次のコードに置き換えます。

public override void Up()
{
    // Drop foreign keys and indexes that point to tables we're going to drop.
    DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student");
    DropIndex("dbo.Enrollment", new[] { "StudentID" });

    RenameTable(name: "dbo.Instructor", newName: "Person");
    AddColumn("dbo.Person", "EnrollmentDate", c => c.DateTime());
    AddColumn("dbo.Person", "Discriminator", c => c.String(nullable: false, maxLength: 128, defaultValue: "Instructor"));
    AlterColumn("dbo.Person", "HireDate", c => c.DateTime());
    AddColumn("dbo.Person", "OldId", c => c.Int(nullable: true));

    // Copy existing Student 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, ID AS OldId FROM dbo.Student");

    // Fix up existing relationships to match new PK's.
    Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator = 'Student')");

    // Remove temporary key
    DropColumn("dbo.Person", "OldId");

    DropTable("dbo.Student");

    // Re-create foreign keys and indexes pointing to new table.
    AddForeignKey("dbo.Enrollment", "StudentID", "dbo.Person", "ID", cascadeDelete: true);
    CreateIndex("dbo.Enrollment", "StudentID");
}

このコードは、次のデータベースの更新タスクを処理します。

  • 外部キー制約と Student テーブルをポイントするインデックスを削除します。

  • Instructor テーブルの名前の Person に変更し、Student データを格納するために必要な変更を加えます。

    • 受講者の null 許容 EnrollmentDate を追加します。
    • 行が、受講者かインストラクターかを示すために識別子列を追加します。
    • 受講者行には雇用日がないので HireDate を nul 許容にします。
    • 受講者をポイントする外部キーの更新に使用する一時的なフィールドを追加します。 Person テーブルに受講者をコピーするときに新しい主キー値を受け取ります。
  • Student テーブルから Person テーブルにデータをコピーします。 これにより、受講者に新しい主キー値が割り当てられます。

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

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

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

update-database コマンドをもう一度実行します。

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

Note

データを移行してスキーマを変更すると、他のエラーが発生する可能性があります。 解決できない移行エラーが発生した場合は、Web.config ファイルの接続文字列を変更するか、データベースを削除すると、チュートリアルを続行できます。 最も簡単な方法は、Web.config ファイルでデータベースの名前を変更することです。 たとえば、次の例に示すように、データベース名を ContosoUniversity2 に変更します。

<add name="SchoolContext" 
    connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=ContosoUniversity2;Integrated Security=SSPI;" 
    providerName="System.Data.SqlClient" />

新しいデータベースには移行するデータがないため、update-database コマンドがエラーなしで完了する可能性がはるかに高くなります。 データベースを削除する方法については、「Visual Studio 2012 からデータベースを削除する方法」をご覧ください。 チュートリアルを続行するためにこの方法を使用する場合は、このチュートリアルの最後にある展開手順をスキップするか、新しいサイトとデータベースに展開します。 既に展開しているのと同じサイトに更新プログラムを展開すると、EF は移行を自動的に実行するときに同じエラーを受け取ります。 移行エラーのトラブルシューティングを行う場合、最適なリソースは、Entity Framework フォーラムまたは StackOverflow.com のいずれかです。

実装をテストする

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

サーバー エクスプローラーで、Data Connections\SchoolContext を展開し、Tables を展開すると、Student テーブルと Instructor テーブルが Person テーブルに置き換えられていることを確認できます。 Person テーブルを展開すると、StudentInstructor テーブルに以前存在していたすべての列があることがわかります。

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

次の図は、新しい School データベースの構造を示しています。

School_database_diagram

Azure に展開する

このセクションでは、このチュートリアル シリーズのパート 3 並べ替え、フィルター処理、ページング内の省略可能なAzure へのアプリのデプロイセクションを完了している必要があります。 移行エラーが発生し、ローカル プロジェクトでデータベースを削除してそれを解決した場合は、この手順をスキップするか、または、新しいサイトとデータベースを作成し、新しい環境に展開してください。

  1. Visual Studio のソリューション エクスプローラーで、プロジェクトを右クリックし、コンテキスト メニューの [発行] をクリックします。

  2. [発行] をクリックします。

    既定のブラウザーで Web アプリが開きます。

  3. アプリケーションをテストし、動作を確認します。

    データベースにアクセスするページを初めて実行すると、Entity Framework は、データベースを現在のデータ モデルに対応させるために必要な移行 Up メソッドをすべて実行します。

コードを取得する

完成したプロジェクトのダウンロード

その他のリソース

他の Entity Framework リソースへのリンクは、「ASP.NET データ アクセス - 推奨リソース」にあります。

この、およびその他の継承構造の詳細については、MSDN の TPT 継承パターンTPH 継承パターンをご覧ください。 次のチュートリアルでは、比較的高度なさまざまな Entity Framework のシナリオを処理する方法を説明します。

次のステップ

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

  • 継承をデータベースにマップする方法について学びました
  • Person クラスを作成した
  • Instructor と Student を更新した
  • モデルに Person を追加しました
  • 移行を作成および更新しました
  • 実装をテストした
  • Azure にデプロイしました

次のチュートリアルに進み、Entity Framework Code First を使用するより高度な ASP.NET Web アプリケーションを開発する際に、注意すべきいくつかのトピックについて学びます。