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
解決できない問題が発生した場合は、完了した章をダウンロードして、問題を再現してみてください。 通常、完成したコードと自分のコードを比較することで、問題の解決策を見つけることができます。 一般的なエラーとその解決方法については、「エラーと回避策」をご覧ください。
前のチュートリアルでは、コンカレンシー例外を処理しました。 このチュートリアルでは、データ モデルで継承を実装する方法を示します。
オブジェクト指向プログラミングでは、継承を使用して冗長コードを排除できます。 このチュートリアルでは、Instructor
と Student
クラスを Person
基底クラスから派生するように変更します。この基底クラスはインストラクターと受講者の両方に共通な LastName
などのプロパティを含んでいます。 どの Web ページも追加または変更しませんが、コードの一部を変更し、それらの変更はデータベースに自動的に反映されます。
Table-per-Hierarchy と Table-Per-Type 継承
オブジェクト指向プログラミングでは、継承を使用して、関連するクラスの操作を容易にすることができます。 たとえば、School
データ モデルの Instructor
および Student
クラスで複数のプロパティを共有すると、冗長コードが作成されます。
Instructor
エンティティと Student
エンティティで共有されるプロパティの冗長なコードを削除すると仮定します。 次の図に示すように、それらの共有プロパティのみが含まれる Person
基底クラスを作成し、Instructor
と Student
エンティティがその基底クラスから継承するようにできます。
データベースでこの継承構造を表すことができるいくつかの方法があります。 受講者とインストラクターの両方に関する情報を 1 つのテーブル内に含む Person
テーブルを使用できます。 一部の列はインストラクター (HireDate
) にのみ、一部は学生 (EnrollmentDate
) にのみ、一部は両方 (LastName
、FirstName
) に適用される可能性があります。 通常、各行がどの種類を表すかを示す "識別子" の列があります。 たとえば、識別子列にインストラクターを示す "Instructor" と受講者を示す "Student" がある場合があります。
1 つのデータベース テーブルからエンティティの継承構造を生成するこのパターンは、Table-per-Hierarchy (TPH) 継承と呼ばれます。
代わりに、継承構造と同じように見えるデータベースを作成することもできます。 たとえば、Person
テーブルに名前フィールドのみを含め、データ フィールドが含まれる別の Instructor
テーブルと Student
テーブルを使用できます。
このエンティティ クラスごとにデータベース テーブルを作成するパターンは、Table-Per-Type (TPT) 継承と呼ばれます。
TPH 継承パターンでは、一般的に TPT 継承パターンよりも高いパフォーマンスを Entity Framework で実現します。これは、TPT パターンの結果として複雑な結合クエリになる可能性があるためです。 このチュートリアルでは、TPH 継承の実装方法を示します。 これを行うには、次の手順を実行じます。
Person
クラスを作成し、Person
の派生元のInstructor
およびStudent
クラスを変更します。- モデルからデータベースへのマッピング コードをデータベース コンテキスト クラスに追加します。
- プロジェクト全体の
InstructorID
とStudentID
参照を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 に必要なのことはこれですべてです。 ご覧のように、データベースが再作成されると、Student
と Instructor
テーブルの代わりに 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 フォルダー内のタイムスタンプ付き移行ファイル内を "除き"、プロジェクト全体で変更InstructorID
を PersonID
、StudentID
を PersonID
に変更する必要があります。 これを行うには、変更する必要があるファイルのみを検索して開き、開いたファイルに対してグローバル変更を実行します。 変更する必要がある Migrations フォルダー内の唯一のファイルは Migrations\Configuration.cs です。
-
重要
まず、Visual Studio で開いているすべてのファイルを閉じます。
[編集] メニューで、[検索と置換] -- [すべてのファイルを検索] を選択してから、
InstructorID
を含むプロジェクト内のすべてのファイルを検索します。各ファイルの 1 行をダブルクリックして、Migrations フォルダーの <time-stamp>_.cs 移行ファイルを "除く" 各ファイルを [検索結果] ウィンドウで開きます。
[フォルダーを指定して置換] ダイアログを開き、[検索対象] を [開いているすべてのドキュメント] に変更します。
[フォルダーを指定して置換] ダイアログを使用して、すべての
InstructorID
をPersonID.
に変更しますStudentID
が含まれているプロジェクト内のすべてのファイルを検索します。各ファイルの 1 行をダブルクリックして、Migrations フォルダーの <time-stamp>_*.cs 移行ファイルを "除く" 各ファイルを [検索結果] ウィンドウで開きます。
[フォルダーを指定して置換] ダイアログを開き、[検索対象] を [開いているすべてのドキュメント] に変更します。
[フォルダーを指定して置換] ダイアログを使用して、すべての
StudentID
をPersonID
に変更します。プロジェクトをビルドします。
(これは、主キーに名前を付けるための 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] を展開し、[テーブル] を展開すると、Student と Instructor テーブルが Person テーブルに置き換えられていることがわかります。 Person テーブルを展開すると、Student と Instructor テーブルに以前存在していたすべての列があることがわかります。
Person テーブルを右クリックし、 [テーブル データの表示] をクリックして識別子列を表示します。
次の図は、新しい School データベースの構造を示しています。
まとめ
Person
、Student
、Instructor
クラスの Table-per-hierarchy 継承が実装されました。 これと他の継承構造の詳細については、Morteza Manavi のブログで継承マッピング戦略に関する記事を参照してください。 次のチュートリアルでは、リポジトリと作業単位パターンを実装するいくつかの方法を確認します。
他の Entity Framework リソースへのリンクは、ASP.NET データ アクセス コンテンツ マップに関するページにあります。