ASP.NET MVC アプリケーションでの Entity Framework での継承の実装 (8/10)

作成者: Tom Dykstra

Contoso University サンプル Web アプリケーションでは、Entity Framework 5 Code First と Visual Studio 2012 を使用して ASP.NET MVC 4 アプリケーションを作成する方法を示します。 チュートリアル シリーズについては、シリーズの最初のチュートリアルをご覧ください。

注意

解決できない問題が発生した場合は、 完了した章をダウンロード して、問題を再現してみてください。 通常、コードを完成したコードと比較することで、問題の解決策を見つけることができます。 一般的なエラーとその解決方法については、「エラーと回避策」を参照してください。

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

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

階層ごとのテーブルと型ごとのテーブルの継承

オブジェクト指向プログラミングでは、継承を使用して、関連するクラスを簡単に操作できます。 たとえば、データ モデルの Instructor クラスと Student クラスは複数のプロパティを School 共有し、その結果、コードが冗長になります。

冗長コードが強調表示された Student クラスと Instructor クラスを示すスクリーンショット。

Instructor エンティティと Student エンティティで共有されるプロパティの冗長なコードを削除すると仮定します。 これらの共有プロパティのみを含む基底クラスを作成Personし、次の図に示すように、 および Student エンティティをその基底クラスから継承できますInstructor

Person クラスから派生した Student クラスと Instructor クラスを示すスクリーンショット。

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

Person エンティティ クラスからの継承構造を示すスクリーンショット。

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

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

Person エンティティ クラスから派生した新しい Instructor および Student データベース テーブルを示すスクリーンショット。

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

TPT の継承パターンは複雑な結合クエリが発生する可能性があるため、TPH 継承パターンは一般に、TPT 継承パターンよりも Entity Framework のパフォーマンスが向上します。 このチュートリアルでは、TPH 継承の実装方法を示します。 これを行うには、次の手順を実行します。

  • クラスをPerson作成し、 クラスと Student クラスを Instructor からPerson派生するように変更します。
  • モデルからデータベースへのマッピング コードをデータベース コンテキスト クラスに追加します。
  • プロジェクト全体で と StudentID 参照を に変更InstructorIDします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 で、 クラスから クラスをInstructorPerson派生させ、キーフィールドと名前フィールドを削除します。 コードは次の例のように表示されます。

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

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

InstructorID と StudentID を PersonID に変更する

SchoolContext.cs の Instructor-Course マッピング ステートメントで、 を にMapRightKey("PersonID")変更MapRightKey("InstructorID")します。

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

次に、移行フォルダーのタイムスタンプ付き移行ファイルを除き、プロジェクト全体で と にPersonID変更StudentIDInstructorIDPersonIDする必要があります。 これを行うには、変更する必要があるファイルのみを見つけて開き、開いたファイルに対してグローバル変更を実行します。 変更する必要がある Migrations フォルダー内の唯一のファイルは 、Migrations\Configuration.cs です。

  1. 重要

    まず、Visual Studio で開いているすべてのファイルを閉じます。

  2. [ 検索と置換] を クリックします。 [編集] メニューのすべてのファイルを検索し、 を含む InstructorIDプロジェクト内のすべてのファイルを検索します。

    [検索と置換] ウィンドウを示すスクリーンショット。講師 I D、現在のプロジェクト、大文字と小文字の区別、単語全体の一致のチェック ボックス、および [すべて検索] ボタンがすべて強調表示されています。

  3. [結果の検索] ウィンドウで、Migrations フォルダー内の <time-stamp>_.cs 移行ファイルを除く各ファイルを開きます。ファイルごとに 1 行をダブルクリックします。

    [結果の検索] ウィンドウを示すスクリーンショット。タイム スタンプ移行ファイルは赤でクロスアウトされます。

  4. [ ファイル内の置換 ] ダイアログを開き、[ 検索先 ] を [ 開いているすべてのドキュメント] に変更します。

  5. [ ファイル内の置換 ] ダイアログを使用して、すべてを InstructorID に変更する PersonID.

    [検索と置換] ウィンドウを示すスクリーンショット。[テキストに置き換える] フィールドにユーザー ID が入力されます。

  6. を含む StudentIDプロジェクト内のすべてのファイルを検索します。

  7. [結果の検索] ウィンドウで、Migrations フォルダー内の <time-stamp>_*.cs 移行ファイルを除く各ファイルを開きます。ファイルごとに 1 行をダブルクリックします。

    [結果の検索] ウィンドウを示すスクリーンショット。タイム スタンプ移行ファイルがクロスアウトされます。

  8. [ ファイル内の置換 ] ダイアログを開き、[ 検索先 ] を [ 開いているすべてのドキュメント] に変更します。

  9. [ ファイル内の置換 ] ダイアログを使用して、すべてを StudentIDPersonID変更します。

    [検索と置換] ウィンドウを示すスクリーンショット。[ファイル]、[開いているすべてのドキュメント]、[大文字と小文字を区別する]、[単語全体に一致] チェック ボックス、および [すべて置換] ボタンが強調表示されています。

  10. プロジェクトをビルドします。

(これは、主キーに名前を付けるパターンのclassnameID欠点を示しています。クラス名の前に付けずに主キー ID という名前を付けた場合、今は名前を変更する必要はありません)。

移行ファイルの作成と更新

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

Add-Migration Inheritance

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

ALTER TABLE ステートメントが FOREIGN KEY 制約 "FK_dboと競合しています。Department_dbo。Person_PersonID"。 競合は、データベース "ContosoUniversity"、テーブル "dbo" で発生しました。Person"、列 'PersonID'。

lt&移行を開きます。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 コマンドをもう一度実行します。

Note

データを移行してスキーマを変更すると、他のエラーが発生する可能性があります。 解決できない移行エラーが発生した場合は、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 エラーなしで完了する可能性がはるかに高くなります。 データベースを削除する方法については、「 How to Drop a Database from Visual Studio 2012」を参照してください。 この方法を使用してチュートリアルを続行する場合は、デプロイされたサイトが移行を自動的に実行するときに同じエラーが発生するため、このチュートリアルの最後の展開手順をスキップします。 移行エラーのトラブルシューティングを行う場合、最適なリソースは Entity Framework フォーラムまたは StackOverflow.com のいずれかです。

テスト

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

[サーバー エクスプローラー] で[SchoolContext] を展開し、[テーブル] を展開すると、Student テーブルと Instructor テーブルが Person テーブルに置き換えられていることがわかります。 Person テーブルを展開すると、Student テーブルと Instructor テーブルに含まれるすべての列が含まれていることがわかります。

[サーバー エクスプローラー] ウィンドウを示すスクリーンショット。[データ接続]、[学校コンテキスト]、[テーブル] タブが展開され、Person テーブルが表示されます。

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

Person テーブルを示すスクリーンショット。識別子列名が強調表示されています。

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

School データベースダイアグラムを示すスクリーンショット。

まとめ

階層ごとのテーブルの継承が、、Studentおよび Instructor クラスに対してPerson実装されました。 この継承構造とその他の継承構造の詳細については、Morteza Manavi のブログの 「継承マッピング戦略 」を参照してください。 次のチュートリアルでは、リポジトリと作業単位のパターンを実装するいくつかの方法について説明します。

他の Entity Framework リソースへのリンクは、 ASP.NET データ アクセス コンテンツ マップにあります。