次の方法で共有


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

著者: Tom Dykstra

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

Note

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

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

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

Table-per-Hierarchy と Table-Per-Type 継承

オブジェクト指向プログラミングでは、継承を使用して、関連するクラスの操作を容易にすることができます。 たとえば、School データ モデルの Instructor および Student クラスで複数のプロパティを共有すると、冗長コードが作成されます。

Screenshots that show the Student and Instructor classes with redundant codes highlighted.

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

Screenshot that shows the Student and Instructor classes deriving from the Person class.

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

Screenshot that shows the inheritance structure from the Person entity class.

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

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

Screenshot that shows new Instructor and Student database tables deriving from the Person entity class.

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

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

  • Person クラスを作成し、Person の派生元の Instructor および Student クラスを変更します。
  • モデルからデータベースへのマッピング コードをデータベース コンテキスト クラスに追加します。
  • プロジェクト全体の InstructorIDStudentID 参照を PersonID に変更します。

Person クラスの作成

注: これらのクラスを使用するコントローラーを更新するまで、以下のクラスを作成した後でプロジェクトをコンパイルすることはできません。

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

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 で、Person エンティティ型の DbSet プロパティを追加します。

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

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

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

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

  1. 重要

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

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

    Screenshot that shows the Find and Replace window. Instructor I D, Current Project, Match case and Match whole word checkboxes, and Find All button are all highlighted.

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

    Screenshot that shows Find Results window. The time stamp migration files are crossed out in red.

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

  5. [フォルダーを指定して置換] ダイアログを使用して、すべての InstructorIDPersonID. に変更します

    Screenshot that shows the Find and Replace window. Person I D is entered in the Replace with text field.

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

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

    Screenshot that shows the Find Results window. The time stamp migration files are crossed out.

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

  9. [フォルダーを指定して置換] ダイアログを使用して、すべての StudentIDPersonID に変更します。

    Screenshot that shows the Find and Replace window. Replace in Files, All Open Documents, Match case and Match whole word checkboxes, and Replace All button are highlighted.

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

(これは、主キーに名前を付けるための classnameID パターンの "欠点" を示しています。クラス名にプレフィックスを付けずに主キー ID の名前を付けた場合に、名前を変更する必要はなくなりました)。

移行ファイルを作成および更新する

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

Add-Migration Inheritance

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

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

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

テスト

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

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

Screenshot that shows the Server Explorer window. The Data Connections, School Context, and Tables tabs are expanded to show the Person table.

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

Screenshot that shows the Person table. The Discriminator column name is highlighted.

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

Screenshot that shows the School database diagram.

まとめ

PersonStudentInstructor クラスの Table-per-hierarchy 継承が実装されました。 これと他の継承構造の詳細については、Morteza Manavi のブログで継承マッピング戦略に関する記事を参照してください。 次のチュートリアルでは、リポジトリと作業単位パターンを実装するいくつかの方法を確認します。

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