前のチュートリアルでは、3 つのエンティティで構成された単純なデータ モデルを使用しました。 このチュートリアルでは、エンティティとリレーションシップをさらに追加し、書式設定、検証、およびデータベース マッピングルールを指定してデータ モデルをカスタマイズします。
完了すると、エンティティ クラスは、以下の図のように完成したデータ モデルを構成します。
このチュートリアルでは、次の操作を行います。
- データ モデルをカスタマイズする
- Student エンティティに変更を加える
- Instructor エンティティの作成
- OfficeAssignment エンティティを作成する
- Course エンティティを変更
- Department エンティティを作成する
- Enrollment エンティティを変更
- データベース コンテキストを更新する
- テストデータでデータベースを初期投入する
- 移行を追加する
- 接続文字列を変更する
- データベースの更新
Prerequisites
データ モデルをカスタマイズする
このセクションでは、書式設定、検証、データベース マッピング規則を指定する属性を使用して、データ モデルをカスタマイズする方法を示します。 次のいくつかのセクションでは、既に作成したクラスに属性を追加し、モデル内の残りのエンティティ型の新しいクラスを作成することで、School データ モデル全体を作成します。
DataType 属性
学生の登録日について、すべての Web ページでは現在、日付と共に時刻が表示されていますが、このフィールドでは日付が重要になります。 データ注釈属性を使用すれば、1 つのコードを変更するだけで、データを表示するすべてのビューの表示形式を修正できます。 その方法例を表示するには、EnrollmentDate クラスの Student プロパティに属性を追加します。
Models/Student.csで、次の例に示すように、using 名前空間の System.ComponentModel.DataAnnotations ステートメントを追加し、DisplayFormat プロパティに EnrollmentDate 属性と DataType 属性を追加します。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
DataType 属性は、データベースの組み込み型よりも具体的なデータ型を指定するために使用されます。 この例では、追跡する必要があるのは、日付と時刻ではなく、日付のみです。
DataType 列挙型は、Date、Time、PhoneNumber、Currency、EmailAddress など、多くの型のために用意されています。 また、DataType 属性を使用して、アプリケーションで型固有の機能を自動的に提供することもできます。 たとえば、mailto: リンクを DataType.EmailAddress に作成したり、HTML5 をサポートするブラウザーで DataType.Date に日付セレクターを提供したりできます。
DataType 属性は、HTML 5 ブラウザーが認識できる HTML 5 data- ("データ ダッシュ" と読みます) 属性を出力します。
DataType属性はバリデーションを提供しません。
DataType.Date は、表示される日付の書式を指定しません。 既定では、データ フィールドは、サーバーの CultureInfo に基づく既定の形式に従って表示されます。
DisplayFormat 属性は、日付の書式を明示的に指定するために使用されます。
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
ApplyFormatInEditMode の設定では、編集用にテキスト ボックスに値を表示するときにも適用する必要がある書式設定を指定します (一部のフィールドでは、たとえば通貨値の場合、編集用のテキスト ボックスに通貨記号を表示したくない場合があります。)
DisplayFormat属性は単独で使用できますが、通常は DataType 属性も使用することをお勧めします。
DataType属性は、画面に表示する方法とは対照的にデータのセマンティクスを伝達し、DisplayFormatでは得られない次の利点を提供します。
ブラウザーは HTML5 機能を有効にすることができます (たとえば、カレンダー コントロール、ロケールに適した通貨記号、メール リンク、一部のクライアント側の入力検証を表示するときなど)。
ブラウザーの既定では、ロケールに基づいて正しい書式を使ってデータがレンダリングされます。
詳細については、 <input> タグ ヘルパーのドキュメントを参照してください。
アプリを実行し、[Students Index] ページに移動し、登録日の時刻が表示されなくなったことを確認します。 Student モデルを使用するビューについても同じことが当てはまります。
StringLength 属性
属性を使用して、データ検証規則と検証エラー メッセージを指定することもできます。
StringLength属性は、データベース内の最大長を設定し、ASP.NET Core MVC のクライアント側とサーバー側の検証を提供します。 この属性で最小長を指定することもできますが、最小値はデータベース スキーマに影響しません。
たとえば、ユーザーが 50 文字を超える名前を入力しないようにする必要があるとします。 この制限を追加するには、次の例に示すように、 StringLength 属性を LastName プロパティと FirstMidName プロパティに追加します。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
StringLength属性では、ユーザーが名前に空白文字を入力することを防ぐことはできません。
RegularExpression属性を使用して、入力に制限を適用できます。 たとえば、次のコードでは、最初の文字が大文字で、残りの文字が英字であることを要求します。
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
MaxLength属性は、StringLength属性と同様の機能を提供しますが、クライアント側の検証は提供しません。
データベース モデルは今や、データベース スキーマの変更が必要になる形に変更されました。 移行を使用して、アプリケーション UI を使用してデータベースに追加したデータを失うことなくスキーマを更新します。
変更を保存し、プロジェクトをビルドします。 次に、プロジェクト フォルダーでコマンド ウィンドウを開き、次のコマンドを入力します。
dotnet ef migrations add MaxLengthOnNames
dotnet ef database update
migrations add コマンドは、変更によって 2 つの列の最大長が短くなるため、データ損失が発生する可能性があることを警告します。 移行により、 <timeStamp>_MaxLengthOnNames.csという名前のファイルが作成されます。 このファイルには、現在のデータ モデルに一致するようにデータベースを更新する Up メソッドのコードが含まれています。
database update コマンドはそのコードを実行しました。
移行ファイル名の先頭に付加されたタイムスタンプは、Entity Framework によって移行の順序付けに使用されます。 update-database コマンドを実行する前に複数の移行を作成し、すべての移行が作成された順序で適用できます。
アプリを実行し、[ 学生 ] タブを選択し、[ 新規作成] をクリックして、いずれかの名前を 50 文字を超えて入力します。 アプリケーションにより、この操作が防止されます。
Column 属性
属性を使用して、データベースへのクラスとプロパティのマッピング方法を制御することもできます。 フィールドにミドル ネームも含まれている場合があるため、名フィールドに対して FirstMidName という名前を使用したとします。 ただし、データベース列は FirstName という名前にする必要があります。これは、データベースに対するアドホック クエリを記述するユーザーがその名前に慣れているためです。 このマッピングを作成する場合、Column 属性を使用できます。
Column 属性は、データベースの作成時に、Student プロパティにマップする FirstMidName テーブルの列が FirstName という名前になるように指定します。 つまり、コードが Student.FirstMidName を参照したときに、データが FirstName テーブルの Student 列から取り込まれるか、更新されます。 列名を指定しない場合は、プロパティ名と同じ名前が付けられます。
Student.cs ファイルで、次の強調表示されたコードに示すように、using のための System.ComponentModel.DataAnnotations.Schema ステートメントを追加し、列名属性を FirstMidName プロパティに追加します。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50)]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Column属性を追加すると、SchoolContextの基盤となるモデルが変更されるため、データベースと一致しません。
変更を保存し、プロジェクトをビルドします。 次に、プロジェクト フォルダーのコマンド ウィンドウを開き、次のコマンドを入力して別のマイグレーションを作成します。
dotnet ef migrations add ColumnFirstName
dotnet ef database update
SQL Server オブジェクト エクスプローラーで、Student テーブルをダブルクリックして Student テーブル デザイナーを開きます。
最初の 2 つの移行を適用する前に、名前列は nvarchar(MAX) 型でした。 現在は nvarchar(50) になり、列名が FirstMidName から FirstName に変更されました。
Note
次のセクションのすべてのエンティティ クラスの作成を完了する前にコンパイルしようとすると、コンパイラ エラーが発生する可能性があります。
Student エンティティの変更
Models/Student.csで、前に追加したコードを次のコードに置き換えます。 変更が強調表示されます。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50)]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public ICollection<Enrollment> Enrollments { get; set; }
}
}
必須属性
Required 属性では、名前プロパティの必須フィールドを作成します。
Required属性は、値型 (DateTime、int、double、float など) などの null 非許容の型には必要ありません。 null にできない型は自動的に必須フィールドとして扱われます。
MinimumLength を有効にするには、Required 属性を MinimumLength と共に使用する必要があります。
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
Display 属性
Display属性は、各インスタンスで、単語を区切るスペースのないプロパティ名の代わりに、テキスト ボックスのキャプションが "First Name"、"Last Name"、"Full Name"、および "Enrollment Date" になることを指定します。
FullName 集計プロパティ
FullName は集計プロパティであり、2 つの別のプロパティを連結して作成される値を返します。 そのため、get アクセサーのみを持ち、FullName 列はデータベースに生成されません。
Instructor エンティティを作成
Models/Instructor.csを作成し、テンプレート コードを次のコードで置き換えてください。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor
{
public int ID { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public ICollection<CourseAssignment> CourseAssignments { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}
}
Student エンティティと Instructor エンティティでは、複数のプロパティが同じであることに注意してください。 このシリーズの後半にある「継承の実装」チュートリアルでは、冗長性を排除するためにこのコードをリファクタリングします。
1 行に複数の属性を配置できるため、 HireDate 属性を次のように記述することもできます。
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
その CourseAssignments と OfficeAssignment のナビゲーション プロパティ
CourseAssignments と OfficeAssignment プロパティはナビゲーション プロパティです。
講師は任意の数のコースを担当できるため、CourseAssignments はコレクションとして定義されます。
public ICollection<CourseAssignment> CourseAssignments { get; set; }
ナビゲーション プロパティが複数のエンティティを保持できる場合、その型は、エントリを追加、削除、および更新できるリストである必要があります。
ICollection<T>、または List<T> や HashSet<T> などの型を指定することができます。
ICollection<T> を指定した場合、EF では既定で HashSet<T> コレクションが作成されます。
これらが CourseAssignment エンティティである理由は、多対多リレーションシップに関する以下のセクションで説明します。
Contoso University のビジネス ルールでは、インストラクターは最大 1 つのオフィスしか持つことができないため、 OfficeAssignment プロパティには 1 つの OfficeAssignment エンティティが保持されます (オフィスが割り当てられていない場合は null になる可能性があります)。
public OfficeAssignment OfficeAssignment { get; set; }
OfficeAssignment エンティティを作成する
次のコードを使用して Models/OfficeAssignment.cs を作成します。
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}
}
Key 属性
Instructor エンティティと OfficeAssignment エンティティの間には一対ゼロまたは一対一のリレーションシップがあります。 オフィスが割り当てられている講師についてのみ、オフィス割り当てが存在します。したがって、その主キーは Instructor エンティティに対する外部キーでもあります。 ただし、Entity Framework では、名前がclassnameIDまたはInstructorID名前付け規則に従っていないため、IDをこのエンティティの主キーとして自動的に認識することはできません。 したがって、Key 属性はキーとして識別するために使用されます。
[Key]
public int InstructorID { get; set; }
エンティティに独自の主キーがあるが、プロパティに classnameID または ID 以外の名前を付ける場合は、 Key 属性を使用することもできます。
既定では、EF は、列が識別リレーションシップ用であるため、キーをデータベース生成ではないキーとして扱います。
Instructor ナビゲーション プロパティ
Instructor エンティティには null 許容の OfficeAssignment ナビゲーション プロパティがあります (インストラクターがオフィスの割り当てを持っていない可能性があるため)。OfficeAssignment エンティティには、null 非許容の Instructor ナビゲーション プロパティがあります (インストラクターなしでは office の割り当ては存在できないため、 InstructorID は null 許容ではありません)。 Instructor エンティティに関連する OfficeAssignment エンティティがある場合、各エンティティはナビゲーション プロパティ内の他のエンティティへの参照を持つことになります。
Instructor ナビゲーション プロパティに [Required] 属性を配置して、関連するインストラクターが存在する必要があることを指定することもできますが、 InstructorID 外部キー (このテーブルのキーでもあります) は null 非許容であるため、これを行う必要はありません。
Course エンティティを変更
Models/Course.csで、前に追加したコードを次のコードに置き換えます。 変更が強調表示されます。
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Title { get; set; }
[Range(0, 5)]
public int Credits { get; set; }
public int DepartmentID { get; set; }
public Department Department { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<CourseAssignment> CourseAssignments { get; set; }
}
}
コース エンティティには、関連する Department エンティティを指す外部キー プロパティ DepartmentID があり、 Department ナビゲーション プロパティがあります。
Entity Framework では、関連エンティティのナビゲーション プロパティがある場合、ユーザーがデータ モデルに外部キー プロパティを追加する必要はありません。 EF は、必要に応じてデータベースに外部キーを自動的に作成し、それらのために外部キー用のシャドウ プロパティを作成します。 ただし、データ モデルに外部キーがある場合は、更新をより簡単かつ効率的に行うことができます。 たとえば、編集する Course エンティティをフェッチする場合、 Department エンティティを読み込まない場合は null になるため、 Course エンティティを更新するときは、最初に Department エンティティをフェッチする必要があります。 外部キー プロパティ DepartmentID がデータ モデルに含まれている場合は、更新前に Department エンティティをフェッチする必要はありません。
DatabaseGenerated 属性
DatabaseGenerated プロパティの None パラメーターを持つ CourseID 属性は、データベースによって生成されるのではなく、ユーザーが主キー値を指定することを指定します。
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
既定では、Entity Framework では、データベースによって主キー値が生成されると想定されます。 あなたがほとんどのシナリオで望むものです。 ただし、Course エンティティの場合、1 つの学科に 1000 シリーズ、別の学科に 2000 シリーズといったユーザー指定のコース番号を使用します。
DatabaseGenerated属性は、行が作成または更新された日付を記録するために使用されるデータベース列の場合と同様に、既定値を生成するために使用することもできます。 詳細については、「生成されたプロパティ」を参照してください。
外部キーとナビゲーション プロパティ
Course エンティティの外部キー プロパティとナビゲーション プロパティには、以下のリレーションシップが反映されます。
コースは 1 つの学科に割り当てられます。したがって、前述の理由により、DepartmentID 外部キーと Department ナビゲーション プロパティが存在します。
public int DepartmentID { get; set; }
public Department Department { get; set; }
コースには任意の数の学生が登録できるため、Enrollments ナビゲーション プロパティはコレクションとなります。
public ICollection<Enrollment> Enrollments { get; set; }
コースは複数のインストラクターによって教えられる可能性があるため、 CourseAssignments ナビゲーション プロパティはコレクションです (型 CourseAssignment については 後で説明します)。
public ICollection<CourseAssignment> CourseAssignments { get; set; }
部門エンティティを作成する
次のコードを使用して Models/Department.cs を作成します。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Column 属性
前の手順では、Column 属性を使用して列名のマッピングを変更しました。
Department エンティティのコードでは、Column属性を使用して SQL データ型マッピングを変更し、データベース内のSQL Server money型を使用して列を定義します。
[Column(TypeName="money")]
public decimal Budget { get; set; }
Entity Framework は、プロパティに対して定義した CLR 型に基づいて適切なSQL Serverデータ型を選択するため、通常、列マッピングは必要ありません。 CLR decimal 型は SQL Server の decimal 型にマップされます。 ただし、この場合、列が通貨の金額を保持し、money データ型の方が適切であることがわかります。
外部キー プロパティとナビゲーション プロパティ
外部キーおよびナビゲーション プロパティには、次のリレーションシップが反映されます。
学科には管理者が存在する場合とそうでない場合があり、管理者は常に講師となります。 したがって、InstructorID プロパティは Instructor エンティティの外部キーとして含まれており、プロパティを null 許容としてマークするために、int 型指定の後に疑問符が追加されます。 ナビゲーション プロパティの名前は Administrator ですが、Instructor エンティティを保持します。
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
学科には複数のコースがある場合があるため、Courses ナビゲーション プロパティがあります:
public ICollection<Course> Courses { get; set; }
Note
規則により、Entity Framework では null 非許容の外部キーと多対多リレーションシップに対して連鎖削除が有効になります。 これにより、循環連鎖削除ルールが発生する可能性があります。これにより、移行を追加しようとすると例外が発生します。 たとえば、Department.InstructorID プロパティを null 許容として定義しなかった場合、EF では、講師を削除したときに学科を削除するように連鎖削除規則が構成されます。これは、発生してほしくない動作です。 ビジネス ルールで InstructorID プロパティを null 非許容にする必要がある場合は、次の fluent API ステートメントを使用してリレーションシップの連鎖削除を無効にする必要があります。
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
Enrollment エンティティを変更する
Models/Enrollment.csで、前に追加したコードを次のコードに置き換えます。
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }
public Course Course { get; set; }
public Student Student { get; set; }
}
}
外部キーおよびナビゲーション プロパティ
外部キー プロパティとナビゲーション プロパティには、次のリレーションシップが反映されます。
登録レコードは単一のコースに対するものであるため、CourseID 外部キー プロパティと Course ナビゲーション プロパティがあります。
public int CourseID { get; set; }
public Course Course { get; set; }
登録レコードは 1 人の学生に対するものであるため、StudentID 外部キー プロパティと Student ナビゲーション プロパティがあります。
public int StudentID { get; set; }
public Student Student { get; set; }
多対多の関係
Student および Course エンティティの間には多対多リレーションシップがあり、Enrollment エンティティはデータベースで "ペイロードがある" 多対多の結合テーブルとして機能します。 "ペイロードあり" とは、 Enrollment テーブルに、結合テーブルの外部キー (この場合は主キーと Grade プロパティ) 以外に追加のデータが含まれていることを意味します。
次の図は、エンティティ図でこれらのリレーションシップがどのようになるかを示しています (この図は Entity Framework Power Tools for EF 6.x を使用して生成されました。図の作成はチュートリアルの一部ではなく、ここでは説明用として使われているだけです。)
各リレーションシップ線の一方の端に 1 が、もう一方の端にアスタリスク (*) があり、1 対多リレーションシップであることを示しています。
Enrollment テーブルに成績情報が含まれていない場合、含める必要があるのは 2 つの外部キー CourseID および StudentID のみです。 その場合、データベース内では、ペイロードを持たない多対多の結合テーブル(つまり純粋な結合テーブル)になります。
InstructorエンティティとCourse エンティティには、このような多対多リレーションシップがあり、次の手順では、ペイロードを含まない結合テーブルとして機能するエンティティ クラスを作成します。
EF Core では、多対多リレーションシップの暗黙的な結合テーブルがサポートされていますが、このチュートリアルは、暗黙的な結合テーブルを使用するように更新されていません。 Razorについては、更新されたこのチュートリアルの Pages バージョンを参照してください。
CourseAssignment エンティティ
次のコードを使用して Models/CourseAssignment.cs を作成します。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class CourseAssignment
{
public int InstructorID { get; set; }
public int CourseID { get; set; }
public Instructor Instructor { get; set; }
public Course Course { get; set; }
}
}
エンティティ名を結合
データベース内での Instructor-to-Courses 多対多リレーションシップには結合テーブルが必要であり、エンティティ セットで表される必要があります。 結合エンティティに EntityName1EntityName2という名前を付けるのが一般的です。この場合は CourseInstructorになります。 ただし、リレーションシップを説明する名前を選択することをお勧めします。 データ モデルは単純な状態で始まり、成長していきます。ペイロードなしの結合も後でペイロードを持つことがよくあります。 わかりやすいエンティティ名で始める場合は、後で名前を変更する必要はありません。 結合エンティティでは、ビジネス ドメインに独自の自然な (場合によっては 1 単語の) 名前を指定することが理想的です。 たとえば、書籍と顧客は評価を通じてリンクできます。 この関係では、 CourseAssignment は CourseInstructorよりも優れた選択肢です。
複合キー
外部キーは null 許容ではなく、テーブルの各行を一意に識別するため、個別の主キーは必要ありません。
InstructorIDプロパティとCourseID プロパティは、複合主キーとして機能する必要があります。 EF に対する複合主キーを識別する唯一の方法は、fluent API を使用することです (属性を使用して行うことはできません)。 複合主キーを構成する方法については、次のセクションで説明します。
複合キーを使用すると、1 つのコースに対して複数の行を持ち、1 人のインストラクターに対して複数の行を持つことができますが、同じインストラクターとコースに対して複数の行を持つことはありません。
Enrollment結合エンティティは、独自の主キーを定義するため、この種類の重複が発生する可能性があります。 このような重複を防ぐために、外部キー フィールドで一意のインデックスを追加するか、CourseAssignment と同様の複合主キーを使用して Enrollment を構成することができます。 詳細については、「インデックス」を参照してください。
データベース コンテキストを更新する
Data/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; }
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<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
このコードは、新しいエンティティを追加し、CourseAssignment エンティティの複合主キーを構成します。
fluent API の代替方法について
OnModelCreating クラスの DbContext メソッドのコードでは、fluent API を使用して EF の動作を構成します。 API は、EF Coreドキュメントの次の例のように、一連のメソッド呼び出しをつなげて 1 つのステートメントにすることがよくあるため、"fluent" と呼ばれます。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
このチュートリアルでは、fluent API は、属性では実行できないデータベース マッピングにのみ使用します。 ただし、fluent API を使用して、属性で実行できる書式設定、検証、およびマッピング規則のほとんどを指定することもできます。
MinimumLength などの一部の属性は fluent API で適用できません。 前述のように、 MinimumLength はスキーマを変更せず、クライアント側とサーバー側の検証規則のみを適用します。
一部の開発者は fluent API のみを使用することを選ぶため、エンティティ クラスを "クリーン" な状態に保つことができます。必要に応じて、属性と fluent API を組み合わせて使用できます。fluent API のみを使用して実行できるカスタマイズがいくつかありますが、一般的は 2 つの方法のいずれかを選択して、できるだけ一貫性を保つためにそれを使用することをお勧めします。 両方の使用時に競合が発生する場合は、Fluent API で属性がオーバーライドされることに注意してください。
属性と fluent API の詳細については、「構成の方法」を参照してください。
リレーションシップを示すエンティティ図
次の図では、完成した School モデルに対して Entity Framework Power Tools で作成される図を示します。
一対多リレーションシップの線 (1 対 *) 以外にも、ここには Instructor および OfficeAssignment エンティティ間の一対ゼロまたは一リレーションシップの線 (1 対 0..1) と、Instructor および Department エンティティ間のゼロまたは一対多リレーションシップの線 (0..1 対 *) があります。
テスト データを使ってデータベースをシードする
作成した新しいエンティティのシード データを提供するために、Data/DbInitializer.cs ファイル内のコードを次のコードに置き換えてください。
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
//context.Database.EnsureCreated();
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}
var students = new Student[]
{
new Student { FirstMidName = "Carson", LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2010-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2012-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice",
EnrollmentDate = DateTime.Parse("2011-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2005-09-01") }
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();
var instructors = new Instructor[]
{
new Instructor { FirstMidName = "Kim", LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11") },
new Instructor { FirstMidName = "Fadi", LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06") },
new Instructor { FirstMidName = "Roger", LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01") },
new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15") },
new Instructor { FirstMidName = "Roger", LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12") }
};
foreach (Instructor i in instructors)
{
context.Instructors.Add(i);
}
context.SaveChanges();
var departments = new Department[]
{
new Department { Name = "English", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Abercrombie").ID },
new Department { Name = "Mathematics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID },
new Department { Name = "Engineering", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Harui").ID },
new Department { Name = "Economics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID }
};
foreach (Department d in departments)
{
context.Departments.Add(d);
}
context.SaveChanges();
var courses = new Course[]
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
},
new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
},
new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
},
new Course {CourseID = 1045, Title = "Calculus", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
},
new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
},
new Course {CourseID = 2021, Title = "Composition", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
},
new Course {CourseID = 2042, Title = "Literature", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
},
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();
var officeAssignments = new OfficeAssignment[]
{
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
Location = "Smith 17" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
Location = "Gowan 27" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
Location = "Thompson 304" },
};
foreach (OfficeAssignment o in officeAssignments)
{
context.OfficeAssignments.Add(o);
}
context.SaveChanges();
var courseInstructors = new CourseAssignment[]
{
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
};
foreach (CourseAssignment ci in courseInstructors)
{
context.CourseAssignments.Add(ci);
}
context.SaveChanges();
var enrollments = new Enrollment[]
{
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").ID,
CourseID = courses.Single(c => c.Title == "Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Justice").ID,
CourseID = courses.Single(c => c.Title == "Literature").CourseID,
Grade = Grade.B
}
};
foreach (Enrollment e in enrollments)
{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
}
}
最初のチュートリアルで見たように、このコードのほとんどは新しいエンティティ オブジェクトを作成し、テストに必要に応じてサンプル データをプロパティに読み込むだけです。 多対多リレーションシップがどのように処理されるかに注目してください。コードでは、Enrollments と CourseAssignment の結合エンティティ セット内にエンティティを作成することで、リレーションシップを作成します。
移行を追加する
変更を保存し、プロジェクトをビルドします。 次に、プロジェクト フォルダーでコマンド ウィンドウを開き、migrations add コマンドを入力します (update-database コマンドはまだ実行しないでください)。
dotnet ef migrations add ComplexDataModel
データ損失の可能性に関する警告が表示されます。
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'
この時点で database update コマンドを実行しようとした場合 (まだ実行しないでください)、次のエラーが表示されます。
ALTER TABLE ステートメントは FOREIGN KEY 制約 "FK_dbo.Course_dbo.Department_DepartmentID" と競合しています。 競合が発生したのは、データベース "ContosoUniversity"、テーブル "dbo.Department"、列 'DepartmentID' です。
既存のデータを使用して移行を実行する場合、外部キー制約を満たすためにスタブ データをデータベースに挿入する必要がある場合があります。
Up メソッドで生成されたコードは、DepartmentID の null 非許容の外部キーを Course テーブルに追加します。 コードの実行時に Course テーブルに既に行がある場合、AddColumn操作は失敗します。SQL Serverは null にできない列に格納する値がわからないためです。 このチュートリアルでは、新しいデータベースで移行を実行しますが、運用アプリケーションでは、移行で既存のデータを処理する必要があるため、次の手順でその方法の例を示します。
この移行を既存のデータと連携させるには、コードを変更して新しい列に既定値を設定し、既定の部署として機能する "Temp" という名前のスタブ部門を作成する必要があります。 その結果、既存の Course 行はすべて、 Up メソッドの実行後に "Temp" 部門に関連付けられます。
{timestamp}_ComplexDataModel.csファイルを開きます。Course テーブルに DepartmentID 列を追加するコード行をコメント アウトします。
migrationBuilder.AlterColumn<string>( name: "Title", table: "Course", maxLength: 50, nullable: true, oldClrType: typeof(string), oldNullable: true); //migrationBuilder.AddColumn<int>( // name: "DepartmentID", // table: "Course", // nullable: false, // defaultValue: 0);Department テーブルを作成するコードの後に、次の強調表示されたコードを追加します。
migrationBuilder.CreateTable( name: "Department", columns: table => new { DepartmentID = table.Column<int>(nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), Budget = table.Column<decimal>(type: "money", nullable: false), InstructorID = table.Column<int>(nullable: true), Name = table.Column<string>(maxLength: 50, nullable: true), StartDate = table.Column<DateTime>(nullable: false) }, constraints: table => { table.PrimaryKey("PK_Department", x => x.DepartmentID); table.ForeignKey( name: "FK_Department_Instructor_InstructorID", column: x => x.InstructorID, principalTable: "Instructor", principalColumn: "ID", onDelete: ReferentialAction.Restrict); }); migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())"); // Default value for FK points to department created above, with // defaultValue changed to 1 in following AddColumn statement. migrationBuilder.AddColumn<int>( name: "DepartmentID", table: "Course", nullable: false, defaultValue: 1);
本番環境のアプリケーションでは、Department 行を追加し、Course 行を新しい Department 行に関連付けるコードまたはスクリプトを記述します。 その後、"Temp" 部署や Course.DepartmentID 列の既定値は不要になります。
変更を保存し、プロジェクトをビルドします。
接続文字列を変更する
DbInitializer クラスに、新しいエンティティのシード データを空のデータベースに追加する新しいコードが追加されました。 EF で新しい空のデータベースを作成するには、appsettings.json の 接続文字列 内のデータベース名を ContosoUniversity3 または使用しているコンピューターでまだ使用していない別の名前に変更します。
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
},
変更を appsettings.json に保存します。
Note
データベース名を変更する代わりに、データベースを削除できます。
SQL Server オブジェクト エクスプローラー (SSOX) または database drop CLI コマンドを使用します:
dotnet ef database drop
データベースの更新
データベース名を変更またはデータベースを削除した後、コマンド ウィンドウで database update コマンドを実行して、移行を実行します。
dotnet ef database update
アプリを実行すると、DbInitializer.Initialize メソッドが実行され、新しいデータベースが設定されます。
前に行ったように SSOX でデータベースを開き、[テーブル] ノードを展開して、すべてのテーブルが作成されていることを確認します。 (SSOX が以前から開いている場合は、[更新] ボタンをクリックしてください。)
アプリを実行して、データベースをシードする初期化子コードをトリガーします。
CourseAssignment テーブルを右クリックし、[データの表示] を選択して、データが含まれているかどうかを確認します。
コードを取得する
次のステップ
このチュートリアルでは、次の操作を行います。
- データ モデルをカスタマイズした
- Student エンティティに対して変更を加えた
- Instructor エンティティを作成しました
- 作成された OfficeAssignment エンティティ
- Course エンティティを変更した
- Department エンティティを作成した
- Enrollment エンティティを変更した
- データベース コンテキストを更新しました
- テスト データを使ってデータベースをシードした
- 移行を追加しました
- 接続文字列を変更しました
- データベースを更新しました
関連データにアクセスする方法の詳細については、次のチュートリアルに進んでください。
ASP.NET Core