パート 5、ASP.NET Core での EF Core を使用した Razor ページ - データ モデル
作成者: Tom Dykstra、Jeremy Likness、Jon P Smith
Contoso 大学 Web アプリでは、EF Core と Visual Studio を使用して Razor Pages Web アプリを作成する方法を示します。 チュートリアル シリーズについては、最初のチュートリアルを参照してください。
解決できない問題が発生した場合は、完成したアプリをダウンロードし、チュートリアルに従って作成した内容とコードを比較します。
前のチュートリアルでは、3 つのエンティティで構成された基本的なデータ モデルを使用して作業を行いました。 このチュートリアルでは、次の作業を行います。
- エンティティとリレーションシップをさらに追加する。
- 書式設定、検証、データベース マッピングの規則を指定して、データ モデルをカスタマイズする。
完成したデータ モデルは、次の図のようになります。
次のデータベース ダイアグラムは Dataedo で作成されました。
Dataedo でデータベース ダイアグラムを作成するには:
- Azure にアプリケーションをデプロイする
- コンピューターに Dataedo をダウンロードしてインストールします。
- 「Azure SQL Database のドキュメントを 5 分で生成する」の手順に従います。
前の Dataedo ダイアグラムでは、CourseInstructor
は Entity Framework によって作成された結合テーブルです。 詳細については、「多対多」を参照してください。
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, ErrorMessage = "First name cannot be longer than 50 characters.")]
[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; }
}
}
前のコードでは、FullName
プロパティが追加され、既存のプロパティに次の属性が追加されます。
FullName 集計プロパティ
FullName
は集計プロパティであり、2 つの別のプロパティを連結して作成される値を返します。 FullName
を設定することはできないので、get アクセサーのみが含まれます。 データベースには FullName
列は作成されません。
DataType 属性
[DataType(DataType.Date)]
学生の登録日について、日付のみが関係しますが、現在はすべての Web ページに日付と共に時刻が表示されています。 データ注釈属性を使用すれば、1 つのコードを変更するだけで、データが表示されるすべてのページの表示形式を修正できます。
DataType 属性では、データベースの組み込み型よりも具体的なデータ型を指定します。 ここでは、日付と時刻ではなく、日付のみを表示する必要があります。 DataType 列挙型は、Date、Time、PhoneNumber、Currency、EmailAddress など、多くのデータ型のために用意されています。また、DataType
属性を使用して、アプリで型固有の機能を自動的に提供することもできます。 次に例を示します。
mailto:
リンクはDataType.EmailAddress
に対して自動的に作成されます。- ほとんどのブラウザーでは、
DataType.Date
に日付セレクターが提供されます。
DataType
属性では、HTML 5 の data-
(データ ダッシュと読みます) 属性が出力されます。 DataType
属性では検証は提供されません。
DisplayFormat 属性
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
DataType.Date
は、表示される日付の書式を指定しません。 既定で、日付フィールドはサーバーの CultureInfo に基づき、既定の書式に従って表示されます。
DisplayFormat
属性は、日付の形式を明示的に指定するために使用されます。 ApplyFormatInEditMode
設定では、書式設定を編集 UI にも適用する必要があることを指定します。 一部のフィールドでは ApplyFormatInEditMode
を使用できません。 たとえば、通貨記号は一般的に編集テキスト ボックスには表示できません。
DisplayFormat
属性は単独で使用できます。 一般的には、DataType
属性を DisplayFormat
属性と一緒に使用することをお勧めします。 DataType
属性は、画面でのレンダリング方法とは異なり、データのセマンティクスを伝達します。 DataType
属性には、DisplayFormat
では得られない以下のような利点があります。
- ブラウザーで HTML5 機能を有効にすることができます。 たとえば、カレンダー コントロール、ロケールに適した通貨記号、メール リンク、クライアント側の入力検証を表示します。
- 既定では、ブラウザーで、ロケールに基づいて正しい書式を使用してデータがレンダリングされます。
詳細については、<入力> タグ ヘルパーに関するドキュメントを参照してください。
StringLength 属性
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
データ検証規則と検証エラー メッセージは、属性を使用して指定できます。 StringLength 属性では、データ フィールドで使用できる最小文字長と最大文字長を指定します。 示されているコードでは、名前で使用可能な文字数が 50 字以下に制限されます。 文字列の最小長を設定する例は、後で示します。
また、StringLength
属性ではクライアント側とサーバー側の検証も提供されます。 最小値は、データベース スキーマに影響しません。
StringLength
属性では、ユーザーが名前に空白を入力しないようにすることはできません。 RegularExpression 属性は、入力に制限を適用するために使用できます。 たとえば、次のコードでは、最初の文字を大文字にし、残りの文字をアルファベット順にすることを要求します。
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
SQL Server オブジェクト エクスプローラー (SSOX) で、Student テーブルをダブルクリックして、Student テーブル デザイナーを開きます。
上の図には Student
テーブルのスキーマが表示されています。 名前フィールドは nvarchar(MAX)
型です。 このチュートリアルの後半で移行を作成して適用すると、文字列長属性の結果として名前フィールドは nvarchar(50)
になります。
Column 属性
[Column("FirstName")]
public string FirstMidName { get; set; }
属性で、データベースへのクラスとプロパティのマッピング方法を制御することができます。 Student
モデルでは、Column
属性を使用して、FirstMidName
プロパティの名前をデータベースの "FirstName" にマッピングします。
データベースが作成されたときに、列名でモデルのプロパティ名が使用されます (Column
属性が使用されている場合を除く)。 Student
モデルでは名フィールドに対して FirstMidName
が使用されます。これは、フィールドにミドル ネームも含まれている場合があるためです。
[Column]
属性により、データ モデル内の Student.FirstMidName
が、Student
テーブルの FirstName
列にマップされます。 Column
属性を追加すると、SchoolContext
をサポートするモデルが変更されます。 SchoolContext
をサポートするモデルはデータベースと一致しなくなります。 そのような不一致は、このチュートリアルの後半で移行を追加することによって解決されます。
Required 属性
[Required]
Required
属性では、名前プロパティの必須フィールドを作成します。 値の型 (例: DateTime
、int
、double
) などの null 非許容型では、Required
属性は必要ありません。 null にできない型は自動的に必須フィールドとして扱われます。
MinimumLength
を適用するには、Required
属性を MinimumLength
と共に使用する必要があります。
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength
と Required
を使用すると、空白で検証を満たすことができます。 文字列を完全に制御するには、RegularExpression
属性を使用します。
Display 属性
[Display(Name = "Last Name")]
Display
属性では、テキスト ボックスのキャプションが "First Name"、"Last Name"、"Full Name"、"Enrollment Date" に指定されます。既定のキャプションには、"Lastname" のように、単語を区切るスペースがありません。
移行を作成する
アプリを実行して [Students] ページに移動します。 例外がスローされます。 [Column]
属性があるため、EF では FirstName
という名前の列が検索されることが予想されますが、データベースの列名はまだ FirstMidName
です。
エラー メッセージは、次のようになります。
SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:
SchoolContext
PMC で、以下のコマンドを入力し、新しい移行を作成してデータベースを更新します。
Add-Migration ColumnFirstName Update-Database
これらの最初のコマンドでは、以下の警告メッセージが生成されます。
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
名前フィールドは現在、50 文字に制限されているため、警告が生成されます。 データベースの名前が 50 文字を超えた場合、51 番目から最後までの文字が失われます。
SSOX で Student テーブルを開きます。
移行が適用される前の名前列の型は nvarchar(MAX) でした。 現在の名前列は
nvarchar(50)
です。 列名はFirstMidName
からFirstName
に変わりました。
- アプリを実行して [Students] ページに移動します。
- 日付と共に時刻が入力および表示されていないことに注意してください。
- [新規作成] を選択し、50 文字を超える名前を入力してみます。
Note
次のセクションでは、いくつかのステージでアプリをビルドします。その場合、コンパイラ エラーが生成されます。 手順では、アプリをビルドするタイミングを指定します。
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<Course> Courses { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}
}
複数の属性を 1 行に配置することができます。 HireDate
属性は次のように記述できます。
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
ナビゲーション プロパティ
Courses
と OfficeAssignment
プロパティはナビゲーション プロパティです。
講師は任意の数のコースを担当できるため、Courses
はコレクションとして定義されます。
public ICollection<Course> Courses { get; set; }
講師は最大で 1 つのオフィスを持つことができるので、OfficeAssignment
プロパティには 1 つの OfficeAssignment
エンティティが保持されます。 オフィスが割り当てられていない場合、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 属性
[Key]
属性は、プロパティ名が classnameID
や ID
以外である場合に、主キー (PK) としてプロパティを識別するために使用されます。
Instructor
エンティティと OfficeAssignment
エンティティの間には一対ゼロまたは一対一のリレーションシップがあります。 オフィスが割り当てられている講師についてのみ、オフィス割り当てが存在します。 OfficeAssignment
PK は、Instructor
エンティティに対する外部キー (FK) でもあります。 一対ゼロまたは一対一のリレーションシップは、あるテーブルの PK が、別のテーブルの PK と FK の両方である場合に発生します。
InstructorID
が ID または classnameID の名前付け規則に従っていないため、EF Core では、InstructorID
を OfficeAssignment
の PK として自動的に認識することはできません。 したがって、Key
属性は PK として InstructorID
を識別するために使用されます。
[Key]
public int InstructorID { get; set; }
列は依存リレーションシップに対するものであるため、既定では EF Core はキーを非データベース生成として扱います。 詳細については、EF キーに関するページを参照してください。
Instructor ナビゲーション プロパティ
特定の講師に対して OfficeAssignment
行が存在しない可能性があるため、Instructor.OfficeAssignment
ナビゲーション プロパティは null でもかまいません。 講師にオフィスが割り当てられていない可能性がある。
外部キー InstructorID
の型は int
であり、null 非許容値型であるため、OfficeAssignment.Instructor
ナビゲーション プロパティには常に講師のエンティティが含まれます。 オフィス割り当ては講師なしでは存在できません。
Instructor
エンティティに関連する OfficeAssignment
エンティティがある場合、各エンティティにはそのナビゲーション プロパティの別のエンティティへの参照があります。
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<Instructor> Instructors { get; set; }
}
}
Course
エンティティには外部キー (FK) プロパティ DepartmentID
があります。 DepartmentID
は関連する Department
エンティティを指します。 Course
エンティティには Department
ナビゲーション プロパティがあります。
EF Core では、モデルに関連エンティティのナビゲーション プロパティがある場合、データ モデルの外部キー プロパティは必要ありません。 EF Core は、必要に応じて、データベースで自動的に FK を作成します。 EF Core は、自動的に作成された FK に対して、シャドウ プロパティを作成します。 ただし、データ モデルに FK を明示的に含めると、更新をより簡単かつ効率的に行うことができます。 たとえば、FK プロパティ DepartmentID
が含まれていない モデルがあるとします。 Course エンティティが編集用にフェッチされた場合は、次のようになります。
- 明示的に読み込まれない場合、
Department
プロパティはnull
になります。 - Course エンティティを更新するには、
Department
エンティティを最初にフェッチする必要があります。
FK エンティティ DepartmentID
がデータ モデルに含まれている場合は、更新前に Department
エンティティをフェッチする必要はありません。
DatabaseGenerated 属性
[DatabaseGenerated(DatabaseGeneratedOption.None)]
属性では、PK をデータベースで生成するのではなく、アプリケーションで提供するように指定します。
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
既定では、EF Core では PK 値がデータベースによって生成されるものと想定されています。 通常は、データベースで生成するのが最善です。 Course
エンティティの場合、PK はユーザーが指定します。 たとえば、数学科の場合は 1000 シリーズ、英文科の場合は 2000 シリーズなどのコース番号となります。
DatabaseGenerated
属性は、既定値を生成する場合にも使用できます。 たとえば、データベースでは、行が作成または更新された日付を記録するための日付フィールドを自動的に生成できます。 詳細については、「生成される値」を参照してください。
外部キー プロパティとナビゲーション プロパティ
Course
エンティティの外部キー (FK) プロパティとナビゲーション プロパティには、以下のリレーションシップが反映されます。
コースが 1 つの学科に割り当てられています。したがって、DepartmentID
FK と Department
ナビゲーション プロパティがあります。
public int DepartmentID { get; set; }
public Department Department { get; set; }
コースには任意の数の学生が登録できるため、Enrollments
ナビゲーション プロパティはコレクションとなります。
public ICollection<Enrollment> Enrollments { get; set; }
コースは複数の講師が担当する場合があるため、Instructors
ナビゲーション プロパティはコレクションとなります。
public ICollection<Instructor> Instructors { get; set; }
Department エンティティ
次のコードを使用して 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 データ型のマッピングを変更するために使用されます。 Budget
列は、データベースでは次のように SQL Server の money 型を使用して定義されます。
[Column(TypeName="money")]
public decimal Budget { get; set; }
通常、列マッピングは必要ありません。 EF Core では、プロパティの CLR 型に基づいて、適切な SQL Server のデータ型が選択されます。 CLR decimal
型は SQL Server の decimal
型にマップされます。 Budget
は通貨用であり、通貨には money データ型がより適しています。
外部キー プロパティとナビゲーション プロパティ
FK およびナビゲーション プロパティには、次のリレーションシップが反映されます。
- 学科には管理者が存在する場合とそうでない場合があります。
- 管理者は常に講師です。 したがって、
InstructorID
プロパティはInstructor
エンティティに対する FK として含まれます。
ナビゲーション プロパティは Administrator
という名前ですが、Instructor
エンティティを保持します。
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
上のコードの ?
は、プロパティが Null 許容であることを示します。
学科には複数のコースがある場合があるため、Courses ナビゲーション プロパティがあります。
public ICollection<Course> Courses { get; set; }
規則により、EF Core では、null 非許容の FK と多対多リレーションシップに対して連鎖削除が有効になります。 この既定の動作では、循環連鎖削除規則が適用される可能性があります。 循環連鎖削除規則が適用されると、移行の追加時に例外が発生します。
たとえば、Department.InstructorID
プロパティが null 非許容として定義されている場合、EF Core では連鎖削除規則が構成されます。 この場合、管理者として割り当てられた講師が削除されると、部門は削除されます。 このシナリオでは、制限規則がより合理的になります。 次の fluent API では、制限規則が設定されて、連鎖削除が無効になります。
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
Enrollment 外部キー プロパティとナビゲーション プロパティ
1 件の登録レコードは、1 人の学生が受講する 1 つのコースに対するものです。
次のコードを使用して Models/Enrollment.cs
を更新します。
using System.ComponentModel.DataAnnotations;
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; }
}
}
FK プロパティとナビゲーション プロパティには、次のリレーションシップが反映されます。
登録レコードは 1 つのコースに対するものであるため、CourseID
FK プロパティと Course
ナビゲーション プロパティがあります。
public int CourseID { get; set; }
public Course Course { get; set; }
登録レコードは 1 人の学生に対するものであるため、StudentID
FK プロパティと Student
ナビゲーション プロパティがあります。
public int StudentID { get; set; }
public Student Student { get; set; }
多対多リレーションシップ
Student
エンティティと Course
エンティティの間には多対多リレーションシップがあります。 Enrollment
エンティティは、データベースで "ペイロードがある" 多対多結合テーブルとして機能します。 "ペイロードがある" とは、Enrollment
テーブルに、結合テーブルの FK 以外に追加データが含まれていることを意味します。 Enrollment
エンティティでは、FK 以外の追加データは PK と Grade
です。
次の図は、エンティティ図でこれらのリレーションシップがどのようになるかを示しています (この図は、EF 6.x 用の EF Power Tools を使用して生成されたものです。このチュートリアルでは図は作成しません)。
各リレーションシップ線の一方の端に 1 が、もう一方の端にアスタリスク (*) があり、1 対多リレーションシップであることを示しています。
Enrollment
テーブルに成績情報が含まれていない場合、含める必要があるのは 2 つの FK (CourseID
と StudentID
) のみです。 ペイロードがない多対多結合テーブルは純粋結合テーブル (PJT) と呼ばれる場合があります。
Instructor
および Course
エンティティには、PJT を使用する多対多リレーションシップがあります。
データベース コンテキストを更新する
次のコードを使用して 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; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable(nameof(Course))
.HasMany(c => c.Instructors)
.WithMany(i => i.Courses);
modelBuilder.Entity<Student>().ToTable(nameof(Student));
modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
}
}
}
前のコードでは、新しいエンティティが追加され、Instructor
エンティティと Course
エンティティの間で多対多リレーションシップ を構成します。
属性の代わりに fluent API を使用する
上のコードの OnModelCreating
メソッドでは、fluent API を使用して EF Core の動作を構成します。 API は "fluent" と呼ばれます。これは、多くの場合、一連のメソッド呼び出しを単一のステートメントにまとめて使用されるためです。 次のコードは fluent API の例です。
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 を混在させることができます。 複合 PK を指定するなど、fluent API でのみ実行できる構成がいくつかあります。 属性 (MinimumLength
) でのみ実行できる構成もいくつかあります。 次のように、fluent API または属性を使用することをお勧めします。
- これら 2 つの方法のいずれかを選択する。
- できるだけ一貫性を保つために選択した方法を使用する。
このチュートリアルで使用する属性のいくつかは、次の用途に使用されます。
- 検証のみ (
MinimumLength
など)。 - EF Core 構成のみ (
HasKey
など)。 - 検証と EF Core の構成 (
[StringLength(50)]
など)。
属性と fluent API の詳細については、「構成の方法」を参照してください。
データベースのシード
Data/DbInitializer.cs
のコードを更新します。
using ContosoUniversity.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}
var alexander = new Student
{
FirstMidName = "Carson",
LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2016-09-01")
};
var alonso = new Student
{
FirstMidName = "Meredith",
LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var anand = new Student
{
FirstMidName = "Arturo",
LastName = "Anand",
EnrollmentDate = DateTime.Parse("2019-09-01")
};
var barzdukas = new Student
{
FirstMidName = "Gytis",
LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var li = new Student
{
FirstMidName = "Yan",
LastName = "Li",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var justice = new Student
{
FirstMidName = "Peggy",
LastName = "Justice",
EnrollmentDate = DateTime.Parse("2017-09-01")
};
var norman = new Student
{
FirstMidName = "Laura",
LastName = "Norman",
EnrollmentDate = DateTime.Parse("2019-09-01")
};
var olivetto = new Student
{
FirstMidName = "Nino",
LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2011-09-01")
};
var students = new Student[]
{
alexander,
alonso,
anand,
barzdukas,
li,
justice,
norman,
olivetto
};
context.AddRange(students);
var abercrombie = new Instructor
{
FirstMidName = "Kim",
LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11")
};
var fakhouri = new Instructor
{
FirstMidName = "Fadi",
LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06")
};
var harui = new Instructor
{
FirstMidName = "Roger",
LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01")
};
var kapoor = new Instructor
{
FirstMidName = "Candace",
LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15")
};
var zheng = new Instructor
{
FirstMidName = "Roger",
LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12")
};
var instructors = new Instructor[]
{
abercrombie,
fakhouri,
harui,
kapoor,
zheng
};
context.AddRange(instructors);
var officeAssignments = new OfficeAssignment[]
{
new OfficeAssignment {
Instructor = fakhouri,
Location = "Smith 17" },
new OfficeAssignment {
Instructor = harui,
Location = "Gowan 27" },
new OfficeAssignment {
Instructor = kapoor,
Location = "Thompson 304" }
};
context.AddRange(officeAssignments);
var english = new Department
{
Name = "English",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = abercrombie
};
var mathematics = new Department
{
Name = "Mathematics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = fakhouri
};
var engineering = new Department
{
Name = "Engineering",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = harui
};
var economics = new Department
{
Name = "Economics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = kapoor
};
var departments = new Department[]
{
english,
mathematics,
engineering,
economics
};
context.AddRange(departments);
var chemistry = new Course
{
CourseID = 1050,
Title = "Chemistry",
Credits = 3,
Department = engineering,
Instructors = new List<Instructor> { kapoor, harui }
};
var microeconomics = new Course
{
CourseID = 4022,
Title = "Microeconomics",
Credits = 3,
Department = economics,
Instructors = new List<Instructor> { zheng }
};
var macroeconmics = new Course
{
CourseID = 4041,
Title = "Macroeconomics",
Credits = 3,
Department = economics,
Instructors = new List<Instructor> { zheng }
};
var calculus = new Course
{
CourseID = 1045,
Title = "Calculus",
Credits = 4,
Department = mathematics,
Instructors = new List<Instructor> { fakhouri }
};
var trigonometry = new Course
{
CourseID = 3141,
Title = "Trigonometry",
Credits = 4,
Department = mathematics,
Instructors = new List<Instructor> { harui }
};
var composition = new Course
{
CourseID = 2021,
Title = "Composition",
Credits = 3,
Department = english,
Instructors = new List<Instructor> { abercrombie }
};
var literature = new Course
{
CourseID = 2042,
Title = "Literature",
Credits = 4,
Department = english,
Instructors = new List<Instructor> { abercrombie }
};
var courses = new Course[]
{
chemistry,
microeconomics,
macroeconmics,
calculus,
trigonometry,
composition,
literature
};
context.AddRange(courses);
var enrollments = new Enrollment[]
{
new Enrollment {
Student = alexander,
Course = chemistry,
Grade = Grade.A
},
new Enrollment {
Student = alexander,
Course = microeconomics,
Grade = Grade.C
},
new Enrollment {
Student = alexander,
Course = macroeconmics,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = calculus,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = trigonometry,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = composition,
Grade = Grade.B
},
new Enrollment {
Student = anand,
Course = chemistry
},
new Enrollment {
Student = anand,
Course = microeconomics,
Grade = Grade.B
},
new Enrollment {
Student = barzdukas,
Course = chemistry,
Grade = Grade.B
},
new Enrollment {
Student = li,
Course = composition,
Grade = Grade.B
},
new Enrollment {
Student = justice,
Course = literature,
Grade = Grade.B
}
};
context.AddRange(enrollments);
context.SaveChanges();
}
}
}
上のコードでは、新しいエンティティのシード データが提供されます。 このコードのほとんどで新しいエンティティ オブジェクトが作成され、サンプル データが読み込まれます。 サンプル データはテストに使用されます。
移行を適用するか、削除して再作成する
既存のデータベースでは、次の 2 つの方法でデータベースを変更できます。
- データベースを削除して再作成する。 SQLite の使用時には、このセクションを選択します。
- 移行を既存のデータベースに適用する。 このセクションの手順は SQL Server にのみ使用でき、SQLite では使用できません。
どちらの選択肢も SQL Server で機能します。 移行適用方法はより複雑で時間がかかりますが、実際の運用環境では推奨される方法です。
データベースを削除して再作成する
EF Core に新しいデータベースを強制的に作成させるには、データベースを削除して更新します。
- Migrations フォルダーを削除します。
- パッケージ マネージャー コンソール (PMC) で、次のコマンドを実行します。
Drop-Database
Add-Migration InitialCreate
Update-Database
アプリを実行します。 アプリを実行すると DbInitializer.Initialize
メソッドが実行されます。 DbInitializer.Initialize
では、新しいデータベースが設定されます。
SSOX でデータベースを開きます。
- SSOX が既に開いている場合は、 [更新] ボタンをクリックします。
- [Tables](テーブル) ノードを展開します。 作成されたテーブルが表示されます。
次の手順
次の 2 つのチュートリアルでは、関連データを読み取って更新する方法について説明します。
前のチュートリアルでは、3 つのエンティティで構成された基本的なデータ モデルを使用して作業を行いました。 このチュートリアルでは、次の作業を行います。
- エンティティとリレーションシップをさらに追加する。
- 書式設定、検証、データベース マッピングの規則を指定して、データ モデルをカスタマイズする。
完成したデータ モデルは、次の図のようになります。
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, ErrorMessage = "First name cannot be longer than 50 characters.")]
[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; }
}
}
前のコードでは、FullName
プロパティが追加され、既存のプロパティに次の属性が追加されます。
[DataType]
[DisplayFormat]
[StringLength]
[Column]
[Required]
[Display]
FullName 集計プロパティ
FullName
は集計プロパティであり、2 つの別のプロパティを連結して作成される値を返します。 FullName
を設定することはできないので、get アクセサーのみが含まれます。 データベースには FullName
列は作成されません。
DataType 属性
[DataType(DataType.Date)]
学生の登録日について、日付のみが関係しますが、現在はすべての Web ページに日付と共に時刻が表示されています。 データ注釈属性を使用すれば、1 つのコードを変更するだけで、データが表示されるすべてのページの表示形式を修正できます。
DataType 属性では、データベースの組み込み型よりも具体的なデータ型を指定します。 ここでは、日付と時刻ではなく、日付のみを表示する必要があります。 DataType 列挙型は、Date、Time、PhoneNumber、Currency、EmailAddress など、多くのデータ型のために用意されています。また、DataType
属性を使用して、アプリで型固有の機能を自動的に提供することもできます。 次に例を示します。
mailto:
リンクはDataType.EmailAddress
に対して自動的に作成されます。- ほとんどのブラウザーでは、
DataType.Date
に日付セレクターが提供されます。
DataType
属性では、HTML 5 の data-
(データ ダッシュと読みます) 属性が出力されます。 DataType
属性では検証は提供されません。
DisplayFormat 属性
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
DataType.Date
は、表示される日付の書式を指定しません。 既定で、日付フィールドはサーバーの CultureInfo に基づき、既定の書式に従って表示されます。
DisplayFormat
属性は、日付の形式を明示的に指定するために使用されます。 ApplyFormatInEditMode
設定では、書式設定を編集 UI にも適用する必要があることを指定します。 一部のフィールドでは ApplyFormatInEditMode
を使用できません。 たとえば、通貨記号は一般的に編集テキスト ボックスには表示できません。
DisplayFormat
属性は単独で使用できます。 一般的には、DataType
属性を DisplayFormat
属性と一緒に使用することをお勧めします。 DataType
属性は、画面でのレンダリング方法とは異なり、データのセマンティクスを伝達します。 DataType
属性には、DisplayFormat
では得られない以下のような利点があります。
- ブラウザーで HTML5 機能を有効にすることができます。 たとえば、カレンダー コントロール、ロケールに適した通貨記号、メール リンク、クライアント側の入力検証を表示します。
- 既定では、ブラウザーで、ロケールに基づいて正しい書式を使用してデータがレンダリングされます。
詳細については、<入力> タグ ヘルパーに関するドキュメントを参照してください。
StringLength 属性
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
データ検証規則と検証エラー メッセージは、属性を使用して指定できます。 StringLength 属性では、データ フィールドで使用できる最小文字長と最大文字長を指定します。 示されているコードでは、名前で使用可能な文字数が 50 字以下に制限されます。 文字列の最小長を設定する例は、後で示します。
また、StringLength
属性ではクライアント側とサーバー側の検証も提供されます。 最小値は、データベース スキーマに影響しません。
StringLength
属性では、ユーザーが名前に空白を入力しないようにすることはできません。 RegularExpression 属性は、入力に制限を適用するために使用できます。 たとえば、次のコードでは、最初の文字を大文字にし、残りの文字をアルファベット順にすることを要求します。
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
SQL Server オブジェクト エクスプローラー (SSOX) で、Student テーブルをダブルクリックして、Student テーブル デザイナーを開きます。
上の図には Student
テーブルのスキーマが表示されています。 名前フィールドは nvarchar(MAX)
型です。 このチュートリアルの後半で移行を作成して適用すると、文字列長属性の結果として名前フィールドは nvarchar(50)
になります。
Column 属性
[Column("FirstName")]
public string FirstMidName { get; set; }
属性で、データベースへのクラスとプロパティのマッピング方法を制御することができます。 Student
モデルでは、Column
属性を使用して、FirstMidName
プロパティの名前をデータベースの "FirstName" にマッピングします。
データベースが作成されたときに、列名でモデルのプロパティ名が使用されます (Column
属性が使用されている場合を除く)。 Student
モデルでは名フィールドに対して FirstMidName
が使用されます。これは、フィールドにミドル ネームも含まれている場合があるためです。
[Column]
属性により、データ モデル内の Student.FirstMidName
が、Student
テーブルの FirstName
列にマップされます。 Column
属性を追加すると、SchoolContext
をサポートするモデルが変更されます。 SchoolContext
をサポートするモデルはデータベースと一致しなくなります。 そのような不一致は、このチュートリアルの後半で移行を追加することによって解決されます。
Required 属性
[Required]
Required
属性では、名前プロパティの必須フィールドを作成します。 値の型 (例: DateTime
、int
、double
) などの null 非許容型では、Required
属性は必要ありません。 null にできない型は自動的に必須フィールドとして扱われます。
MinimumLength
を適用するには、Required
属性を MinimumLength
と共に使用する必要があります。
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength
と Required
を使用すると、空白で検証を満たすことができます。 文字列を完全に制御するには、RegularExpression
属性を使用します。
Display 属性
[Display(Name = "Last Name")]
Display
属性では、テキスト ボックスのキャプションが "First Name"、"Last Name"、"Full Name"、"Enrollment Date" に指定されます。既定のキャプションには、"Lastname" のように、単語を区切るスペースがありません。
移行を作成する
アプリを実行して [Students] ページに移動します。 例外がスローされます。 [Column]
属性があるため、EF では FirstName
という名前の列が検索されることが予想されますが、データベースの列名はまだ FirstMidName
です。
エラー メッセージは、次のようになります。
SqlException: Invalid column name 'FirstName'.
PMC で、以下のコマンドを入力し、新しい移行を作成してデータベースを更新します。
Add-Migration ColumnFirstName Update-Database
これらの最初のコマンドでは、以下の警告メッセージが生成されます。
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
名前フィールドは現在、50 文字に制限されているため、警告が生成されます。 データベースの名前が 50 文字を超えた場合、51 番目から最後までの文字が失われます。
SSOX で Student テーブルを開きます。
移行が適用される前の名前列の型は nvarchar(MAX) でした。 現在の名前列は
nvarchar(50)
です。 列名はFirstMidName
からFirstName
に変わりました。
- アプリを実行して [Students] ページに移動します。
- 日付と共に時刻が入力および表示されていないことに注意してください。
- [新規作成] を選択し、50 文字を超える名前を入力してみます。
Note
次のセクションでは、いくつかのステージでアプリをビルドします。その場合、コンパイラ エラーが生成されます。 手順では、アプリをビルドするタイミングを指定します。
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; }
}
}
複数の属性を 1 行に配置することができます。 HireDate
属性は次のように記述できます。
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
ナビゲーション プロパティ
CourseAssignments
と OfficeAssignment
プロパティはナビゲーション プロパティです。
講師は任意の数のコースを担当できるため、CourseAssignments
はコレクションとして定義されます。
public ICollection<CourseAssignment> CourseAssignments { get; set; }
講師は最大で 1 つのオフィスを持つことができるので、OfficeAssignment
プロパティには 1 つの OfficeAssignment
エンティティが保持されます。 オフィスが割り当てられていない場合、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 属性
[Key]
属性は、プロパティ名が classnameID や ID 以外である場合に、主キー (PK) としてプロパティを識別するために使用されます。
Instructor
エンティティと OfficeAssignment
エンティティの間には一対ゼロまたは一対一のリレーションシップがあります。 オフィスが割り当てられている講師についてのみ、オフィス割り当てが存在します。 OfficeAssignment
PK は、Instructor
エンティティに対する外部キー (FK) でもあります。
InstructorID
が ID または classnameID の名前付け規則に従っていないため、EF Core では、InstructorID
を OfficeAssignment
の PK として自動的に認識することはできません。 したがって、Key
属性は PK として InstructorID
を識別するために使用されます。
[Key]
public int InstructorID { get; set; }
列は依存リレーションシップに対するものであるため、既定では EF Core はキーを非データベース生成として扱います。
Instructor ナビゲーション プロパティ
特定の講師に対して OfficeAssignment
行が存在しない可能性があるため、Instructor.OfficeAssignment
ナビゲーション プロパティは null でもかまいません。 講師にオフィスが割り当てられていない可能性がある。
外部キー InstructorID
の型は int
であり、null 非許容値型であるため、OfficeAssignment.Instructor
ナビゲーション プロパティには常に講師のエンティティが含まれます。 オフィス割り当ては講師なしでは存在できません。
Instructor
エンティティに関連する OfficeAssignment
エンティティがある場合、各エンティティにはそのナビゲーション プロパティの別のエンティティへの参照があります。
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; }
}
}
Course
エンティティには外部キー (FK) プロパティ DepartmentID
があります。 DepartmentID
は関連する Department
エンティティを指します。 Course
エンティティには Department
ナビゲーション プロパティがあります。
EF Core では、モデルに関連エンティティのナビゲーション プロパティがある場合、データ モデルの外部キー プロパティは必要ありません。 EF Core は、必要に応じて、データベースで自動的に FK を作成します。 EF Core は、自動的に作成された FK に対して、シャドウ プロパティを作成します。 ただし、データ モデルに FK を明示的に含めると、更新をより簡単かつ効率的に行うことができます。 たとえば、FK プロパティ DepartmentID
が含まれていない モデルがあるとします。 Course エンティティが編集用にフェッチされた場合は、次のようになります。
- 明示的に読み込まれない場合、
Department
プロパティは null になります。 - Course エンティティを更新するには、
Department
エンティティを最初にフェッチする必要があります。
FK エンティティ DepartmentID
がデータ モデルに含まれている場合は、更新前に Department
エンティティをフェッチする必要はありません。
DatabaseGenerated 属性
[DatabaseGenerated(DatabaseGeneratedOption.None)]
属性では、PK をデータベースで生成するのではなく、アプリケーションで提供するように指定します。
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
既定では、EF Core では PK 値がデータベースによって生成されるものと想定されています。 通常は、データベースで生成するのが最善です。 Course
エンティティの場合、PK はユーザーが指定します。 たとえば、数学科の場合は 1000 シリーズ、英文科の場合は 2000 シリーズなどのコース番号となります。
DatabaseGenerated
属性は、既定値を生成する場合にも使用できます。 たとえば、データベースでは、行が作成または更新された日付を記録するための日付フィールドを自動的に生成できます。 詳細については、「生成される値」を参照してください。
外部キー プロパティとナビゲーション プロパティ
Course
エンティティの外部キー (FK) プロパティとナビゲーション プロパティには、以下のリレーションシップが反映されます。
コースが 1 つの学科に割り当てられています。したがって、DepartmentID
FK と Department
ナビゲーション プロパティがあります。
public int DepartmentID { get; set; }
public Department Department { get; set; }
コースには任意の数の学生が登録できるため、Enrollments
ナビゲーション プロパティはコレクションとなります。
public ICollection<Enrollment> Enrollments { get; set; }
コースは複数の講師が担当する場合があるため、CourseAssignments
ナビゲーション プロパティはコレクションとなります。
public ICollection<CourseAssignment> CourseAssignments { get; set; }
CourseAssignment
については、後で説明します。
Department エンティティ
次のコードを使用して 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 データ型のマッピングを変更するために使用されます。 Budget
列は、データベースでは次のように SQL Server の money 型を使用して定義されます。
[Column(TypeName="money")]
public decimal Budget { get; set; }
通常、列マッピングは必要ありません。 EF Core では、プロパティの CLR 型に基づいて、適切な SQL Server のデータ型が選択されます。 CLR decimal
型は SQL Server の decimal
型にマップされます。 Budget
は通貨用であり、通貨には money データ型がより適しています。
外部キー プロパティとナビゲーション プロパティ
FK およびナビゲーション プロパティには、次のリレーションシップが反映されます。
- 学科には管理者が存在する場合とそうでない場合があります。
- 管理者は常に講師です。 したがって、
InstructorID
プロパティはInstructor
エンティティに対する FK として含まれます。
ナビゲーション プロパティは Administrator
という名前ですが、Instructor
エンティティを保持します。
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
上のコードの疑問符 (?) は、プロパティが null 許容であることを示します。
学科には複数のコースがある場合があるため、Courses ナビゲーション プロパティがあります。
public ICollection<Course> Courses { get; set; }
規則により、EF Core では、null 非許容の FK と多対多リレーションシップに対して連鎖削除が有効になります。 この既定の動作では、循環連鎖削除規則が適用される可能性があります。 循環連鎖削除規則が適用されると、移行の追加時に例外が発生します。
たとえば、Department.InstructorID
プロパティが null 非許容として定義されている場合、EF Core では連鎖削除規則が構成されます。 この場合、管理者として割り当てられた講師が削除されると、部門は削除されます。 このシナリオでは、制限規則がより合理的になります。 次の fluent API では、制限規則が設定されて、連鎖削除が無効になります。
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
Enrollment エンティティ
1 件の登録レコードは、1 人の学生が受講する 1 つのコースに対するものです。
次のコードを使用して 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; }
}
}
外部キー プロパティとナビゲーション プロパティ
FK プロパティとナビゲーション プロパティには、次のリレーションシップが反映されます。
登録レコードは 1 つのコースに対するものであるため、CourseID
FK プロパティと Course
ナビゲーション プロパティがあります。
public int CourseID { get; set; }
public Course Course { get; set; }
登録レコードは 1 人の学生に対するものであるため、StudentID
FK プロパティと Student
ナビゲーション プロパティがあります。
public int StudentID { get; set; }
public Student Student { get; set; }
多対多リレーションシップ
Student
エンティティと Course
エンティティの間には多対多リレーションシップがあります。 Enrollment
エンティティは、データベースでペイロードがある多対多結合テーブルとして機能します。 "ペイロードがある" とは、Enrollment
テーブルに、結合テーブルの FK 以外に追加データが含まれていることを意味します (ここでは PK と Grade
)。
次の図は、エンティティ図でこれらのリレーションシップがどのようになるかを示しています (この図は、EF 6.x 用の EF Power Tools を使用して生成されたものです。このチュートリアルでは図は作成しません)。
各リレーションシップ線の一方の端に 1 が、もう一方の端にアスタリスク (*) があり、1 対多リレーションシップであることを示しています。
Enrollment
テーブルに成績情報が含まれていない場合、含める必要があるのは 2 つの FK (CourseID
と StudentID
) のみです。 ペイロードがない多対多結合テーブルは純粋結合テーブル (PJT) と呼ばれる場合があります。
Instructor
および Course
エンティティには、純粋結合テーブルを使用する多対多リレーションシップがあります。
注: EF 6.x では多対多リレーションシップの暗黙の結合テーブルがサポートされますが、EF Core ではサポートされません。 詳細については、EF Core 2.0 での多対多リレーションシップに関するページを参照してください。
CourseAssignment エンティティ
次のコードを使用して Models/CourseAssignment.cs
を作成します。
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; }
}
}
講師とコースの多対多リレーションシップには結合テーブルが必要であり、その結合テーブルのエンティティは CourseAssignment です。
結合エンティティには EntityName1EntityName2
という名前が付けるのが一般的です。 たとえば、このパターンを使用する講師対コースの結合テーブルは CourseInstructor
になります。 ただし、リレーションシップを説明する名前を使用することをお勧めします。
データ モデルは始めは単純なものであっても大きくなります。 ペイロードがない結合テーブル (PJT) は、頻繁に更新されてペイロードが追加されます。 最初にわかりやすいエンティティ名を付けておけば、結合テーブルが変更されたときに名前を変更する必要はありません。 結合エンティティでは、ビジネス ドメインに独自の自然な (場合によっては 1 単語の) 名前を指定することが理想的です。 たとえば、Books と Customers は Ratings という結合エンティティでリンクできます。 講師対コースの多対多リレーションシップの場合、CourseAssignment
は CourseInstructor
より優先されます。
複合キー
CourseAssignment
の 2 つの FK (InstructorID
と CourseID
) を組み合わせて使用し、CourseAssignment
テーブルの各行を一意に識別します。 CourseAssignment
には専用の PK は必要ありません。 InstructorID
および CourseID
プロパティは複合 PK として機能します。 EF Core に複合 PK を指定する唯一の方法は、fluent API を使用することです。 次のセクションでは、複合 PK の構成方法を示します。
複合キーにより、次のことが保証されます。
- 1 つのコースに対して複数の行が許可される。
- 1 人の講師に対して複数の行が許可される。
- 同じ講師とコースに対して複数の行が許可されない。
Enrollment
結合エンティティでは独自の PK を定義するため、このような重複が考えられます。 このような重複を防ぐには、次のようにします。
- FK フィールドに一意のインデックスを追加する。または
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
エンティティの複合 PK が構成されます。
属性の代わりに fluent API を使用する
上のコードの OnModelCreating
メソッドでは、fluent API を使用して EF Core の動作を構成します。 API は "fluent" と呼ばれます。これは、多くの場合、一連のメソッド呼び出しを単一のステートメントにまとめて使用されるためです。 次のコードは fluent API の例です。
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 を混在させることができます。 (複合 PK を指定して) fluent API でのみ実行できる構成がいくつかあります。 属性 (MinimumLength
) でのみ実行できる構成もいくつかあります。 次のように、fluent API または属性を使用することをお勧めします。
- これら 2 つの方法のいずれかを選択する。
- できるだけ一貫性を保つために選択した方法を使用する。
このチュートリアルで使用する属性のいくつかは、次の用途に使用されます。
- 検証のみ (
MinimumLength
など)。 - EF Core 構成のみ (
HasKey
など)。 - 検証と EF Core の構成 (
[StringLength(50)]
など)。
属性と fluent API の詳細については、「構成の方法」を参照してください。
エンティティ図
次の図では、完成した School モデルに対して EF Power Tools で作成される図を示します。
上の図には以下が示されています。
- いくつかの一対多リレーションシップの線 (1 対 *)。
Instructor
エンティティとOfficeAssignment
エンティティの間の一対ゼロまたは一対一リレーションシップの線 (1 対 0..1)。Instructor
エンティティとDepartment
エンティティの間のゼロ対一またはゼロ対多リレーションシップの線 (1 対 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("2016-09-01") },
new Student { FirstMidName = "Meredith", LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2018-09-01") },
new Student { FirstMidName = "Arturo", LastName = "Anand",
EnrollmentDate = DateTime.Parse("2019-09-01") },
new Student { FirstMidName = "Gytis", LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2018-09-01") },
new Student { FirstMidName = "Yan", LastName = "Li",
EnrollmentDate = DateTime.Parse("2018-09-01") },
new Student { FirstMidName = "Peggy", LastName = "Justice",
EnrollmentDate = DateTime.Parse("2017-09-01") },
new Student { FirstMidName = "Laura", LastName = "Norman",
EnrollmentDate = DateTime.Parse("2019-09-01") },
new Student { FirstMidName = "Nino", LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2011-09-01") }
};
context.Students.AddRange(students);
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") }
};
context.Instructors.AddRange(instructors);
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 }
};
context.Departments.AddRange(departments);
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
},
};
context.Courses.AddRange(courses);
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" },
};
context.OfficeAssignments.AddRange(officeAssignments);
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
},
};
context.CourseAssignments.AddRange(courseInstructors);
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
と CourseAssignments
を参照してください。
移行を追加する
プロジェクトをビルドします。
PMC で、次のコマンドを実行します。
Add-Migration ComplexDataModel
上のコマンドは、考えられるデータ損失に関する警告を表示します。
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
To undo this action, use 'ef migrations remove'
database update
コマンドを実行すると、次のエラーが生成されます。
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.
次のセクションでは、このエラーの対処方法を説明します。
移行を適用するか、削除して再作成する
既存のデータベースができたので、変更を適用する方法について検討する必要があります。 このチュートリアルでは、2 つの方法を示します。
- データベースを削除して再作成する。 SQLite を使用している場合は、このセクションを選択します。
- 移行を既存のデータベースに適用する。 このセクションの手順は SQL Server にのみ使用でき、SQLite では使用できません。
どちらの選択肢も SQL Server で機能します。 移行適用方法はより複雑で時間がかかりますが、実際の運用環境では推奨される方法です。
データベースを削除して再作成する
SQL Server を使用していて、次のセクションの移行適用方法を実行する場合は、このセクションをスキップします。
EF Core に新しいデータベースを強制的に作成させるには、データベースを削除して更新します。
パッケージ マネージャー コンソール (PMC) で、次のコマンドを実行します。
Drop-Database
Migrations フォルダーを削除し、次のコマンドを実行します。
Add-Migration InitialCreate Update-Database
アプリを実行します。 アプリを実行すると DbInitializer.Initialize
メソッドが実行されます。 DbInitializer.Initialize
では、新しいデータベースが設定されます。
SSOX でデータベースを開きます。
SSOX が既に開いている場合は、 [更新] ボタンをクリックします。
[Tables](テーブル) ノードを展開します。 作成されたテーブルが表示されます。
CourseAssignment テーブルを確認します。
- CourseAssignment テーブルを右クリックして、 [データの表示] を選択します。
- CourseAssignment テーブルにデータが含まれていることを確認します。
移行を適用する
このセクションは省略可能です。 以下の手順は、SQL Server LocalDB に対してだけ、前の「データベースを削除して再作成する」セクションをスキップした場合にのみ、使用できます。
既存のデータで移行が実行されている場合、既存のデータでは満たされない FK 制約が存在する可能性があります。 運用データを使用する場合は、既存のデータを移行するための手順を実行する必要があります。 このセクションでは、FK 制約違反の修正例を示します。 これらのコードをバックアップせずに変更しないでください。 前の「データベースを削除して再作成する」セクションを完了している場合は、これらのコードを変更しないでください。
{timestamp}_ComplexDataModel.cs
ファイルには、次のコードが含まれています。
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
type: "int",
nullable: false,
defaultValue: 0);
上のコードでは、null 非許容の DepartmentID
FK が Course
テーブルに追加されます。 前のチュートリアルのデータベースには Course
の行が含まれるため、テーブルを移行して更新することはできません。
既存のデータを使用して ComplexDataModel
の移行を実行するには、次のようにします。
- コードを変更して、新しい列 (
DepartmentID
) に既定値を設定します。 - "Temp" という名前の偽の学科を作成し、既定の学科として機能するようにします。
外部キー制約を修正する
ComplexDataModel
移行クラスで、Up
メソッドを次のように更新します。
{timestamp}_ComplexDataModel.cs
ファイルを開きます。DepartmentID
列をCourse
テーブルに追加するコードの行をコメントアウトします。
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);
次の強調表示されたコードを追加します。 新しいコードが .CreateTable( name: "Department"
ブロックの後に配置されます。
migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(type: "int", nullable: true),
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(type: "datetime2", 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);
前述の変更に伴い、既存の Course
行が、ComplexDataModel.Up
メソッドの実行後に "Temp" 学科に関連付けられます。
ここで示す状況の処理方法は、このチュートリアルのために簡略化されています。 運用アプリは次のことを行います。
- コードまたはスクリプトを組み込み、
Department
行と関連するCourse
行を新しいDepartment
行に追加します。 Course.DepartmentID
の既定値や "Temp" 学科は使用しません。
パッケージ マネージャー コンソール (PMC) で、次のコマンドを実行します。
Update-Database
DbInitializer.Initialize
メソッドは空のデータベースでのみ動作するように設計されているので、Student テーブルと Course テーブルのすべての行を削除するには SSOX を使用します。 (連鎖削除によって Enrollment テーブルが処理されます。)
アプリを実行します。 アプリを実行すると DbInitializer.Initialize
メソッドが実行されます。 DbInitializer.Initialize
では、新しいデータベースが設定されます。
次の手順
次の 2 つのチュートリアルでは、関連データを読み取って更新する方法について説明します。
前のチュートリアルでは、3 つのエンティティで構成された基本的なデータ モデルを使用して作業を行いました。 このチュートリアルでは、次の作業を行います。
- エンティティとリレーションシップをさらに追加する。
- 書式設定、検証、データベース マッピングの規則を指定して、データ モデルをカスタマイズする。
完成したデータ モデルのエンティティ クラスは、次の図のようになります。
解決できない問題が発生した場合は、完成したアプリをダウンロードしてください。
属性を使用してデータ モデルをカスタマイズする
このセクションでは、属性を使用してデータ モデルをカスタマイズします。
DataType 属性
学生のページには現在、登録日の時刻が表示されています。 通常、日付フィールドには日付のみが表示され、時刻は表示されません。
次の強調表示されているコードを使用して、Models/Student.cs
を更新します。
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
に対して自動的に作成されます。- ほとんどのブラウザーでは、
DataType.Date
に日付セレクターが提供されます。
DataType
属性は、HTML 5 ブラウザーが使用する HTML 5 data-
(データ ダッシュと読む) 属性を出力します。 DataType
属性では検証は提供されません。
DataType.Date
は、表示される日付の書式を指定しません。 既定で、日付フィールドはサーバーの CultureInfo に基づき、既定の書式に従って表示されます。
DisplayFormat
属性は、日付の書式を明示的に指定するために使用されます。
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
ApplyFormatInEditMode
設定では、書式設定を編集 UI にも適用する必要があることを指定します。 一部のフィールドでは ApplyFormatInEditMode
を使用できません。 たとえば、通貨記号は一般的に編集テキスト ボックスには表示できません。
DisplayFormat
属性は単独で使用できます。 一般的には、DataType
属性を DisplayFormat
属性と一緒に使用することをお勧めします。 DataType
属性は、画面でのレンダリング方法とは異なり、データのセマンティクスを伝達します。 DataType
属性には、DisplayFormat
では得られない以下のような利点があります。
- ブラウザーで HTML5 機能を有効にすることができます。 たとえば、カレンダー コントロール、ロケールに適した通貨記号、メール リンク、クライアント側の入力検証などを表示します。
- 既定では、ブラウザーで、ロケールに基づいて正しい書式を使用してデータがレンダリングされます。
詳細については、<入力> タグ ヘルパーに関するドキュメントを参照してください。
アプリを実行します。 Students インデックス ページに移動します。 時刻は表示されなくなりました。 Student
モデルを使用するすべてのビューに、時刻なしの日付が表示されます。
StringLength 属性
データ検証規則と検証エラー メッセージは、属性を使用して指定できます。 StringLength 属性では、データ フィールドで使用できる最小文字長と最大文字長を指定します。 また、StringLength
属性ではクライアント側とサーバー側の検証も提供されます。 最小値は、データベース スキーマに影響しません。
Student
モデルを次のコードで更新します。
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, ErrorMessage = "First name cannot be longer than 50 characters.")]
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; }
}
}
上のコードでは、名前で使用可能な文字数を 50 に制限します。 StringLength
属性では、ユーザーが名前に空白を入力しないようにすることはできません。 RegularExpression 属性は、入力に制限を適用するために使用されます。 たとえば、次のコードでは、最初の文字を大文字にし、残りの文字をアルファベット順にすることを要求します。
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
次のようにアプリを実行します。
- Students のページに移動します。
- [新規作成] を選択し、50 文字を超える名前を入力します。
- [作成] を選択すると、クライアント側の検証でエラー メッセージが表示されます。
SQL Server オブジェクト エクスプローラー (SSOX) で、Student テーブルをダブルクリックして、Student テーブル デザイナーを開きます。
上の図には Student
テーブルのスキーマが表示されています。 DB では移行が実行されていないため、名前フィールドに nvarchar(MAX)
型があります。 このチュートリアルの後半で移行が実行されたときに、名前フィールドが nvarchar(50)
になります。
Column 属性
属性で、データベースへのクラスとプロパティのマッピング方法を制御することができます。 このセクションでは、Column
属性を使用して、FirstMidName
プロパティの名前を DB の "FirstName" にマッピングします。
DB が作成されたときに、列名でモデルのプロパティ名が使用されます (Column
属性が使用されている場合を除く)。
Student
モデルでは名フィールドに対して FirstMidName
が使用されます。これは、フィールドにミドル ネームも含まれている場合があるためです。
以下の強調表示されているコードを使用して、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; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[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; }
}
}
前述の変更に伴い、アプリの Student.FirstMidName
は Student
テーブルの FirstName
列にマップされます。
Column
属性を追加すると、SchoolContext
をサポートするモデルが変更されます。 SchoolContext
をサポートするモデルはデータベースと一致しなくなります。 移行を適用する前にアプリを実行した場合は、次の例外が生成されます。
SqlException: Invalid column name 'FirstName'.
DB を更新するには、次のようにします。
- プロジェクトをビルドします。
- プロジェクト フォルダーでコマンド ウィンドウを開きます。 以下のコマンドを入力し、新しい移行を作成して DB を更新します。
Add-Migration ColumnFirstName
Update-Database
migrations add ColumnFirstName
コマンドでは、以下の警告メッセージが生成されます。
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
名前フィールドは現在、50 文字に制限されているため、警告が生成されます。 DB の名前が 50 文字を超えた場合、51 番目から最後までの文字が失われます。
- アプリをテストします。
SSOX で Student テーブルを開きます。
移行が適用される前の名前列の型は 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, ErrorMessage = "First name cannot be longer than 50 characters.")]
[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
など) などの null 非許容型では Required
属性は必要ありません。 null にできない型は自動的に必須フィールドとして扱われます。
Required
属性は、StringLength
属性の最小長パラメーターに置き換えることができます。
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
Display 属性
Display
属性では、テキスト ボックスのキャプションが "First Name"、"Last Name"、"Full Name"、"Enrollment Date" に指定されます。既定のキャプションには、"Lastname" のように、単語を区切るスペースがありません。
FullName 集計プロパティ
FullName
は集計プロパティであり、2 つの別のプロパティを連結して作成される値を返します。 FullName
を設定することはできません。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; }
}
}
複数の属性を 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 Core では既定で HashSet<T>
コレクションが作成されます。
CourseAssignment
エンティティについては、多対多リレーションシップのセクションで説明します。
Contoso University のビジネス ルールには、講師は 1 つのオフィスのみを持つことができると示されています。 OfficeAssignment
プロパティでは単一の OfficeAssignment
エンティティが保持されます。 オフィスが割り当てられていない場合、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 属性
[Key]
属性は、プロパティ名が classnameID や ID 以外である場合に、主キー (PK) としてプロパティを識別するために使用されます。
Instructor
エンティティと OfficeAssignment
エンティティの間には一対ゼロまたは一対一のリレーションシップがあります。 オフィスが割り当てられている講師についてのみ、オフィス割り当てが存在します。 OfficeAssignment
PK は、Instructor
エンティティに対する外部キー (FK) でもあります。 EF Core では、以下の理由で、OfficeAssignment
の PK として InstructorID
を自動的に認識することはできません。
InstructorID
は、ID や classnameID の名前付け規則には従っていません。
したがって、Key
属性は PK として InstructorID
を識別するために使用されます。
[Key]
public int InstructorID { get; set; }
列は依存リレーションシップに対するものであるため、既定では EF Core はキーを非データベース生成として扱います。
Instructor ナビゲーション プロパティ
Instructor
エンティティの OfficeAssignment
ナビゲーション プロパティは、以下の理由で null 許容型となります。
- 参照型 (クラスが null 許容型である)。
- 講師にオフィスが割り当てられていない可能性がある。
OfficeAssignment
エンティティには、以下の理由で null 非許容の Instructor
ナビゲーション プロパティがあります。
InstructorID
は null 非許容です。- オフィス割り当ては講師なしでは存在できません。
Instructor
エンティティに関連する OfficeAssignment
エンティティがある場合、各エンティティにはそのナビゲーション プロパティの別のエンティティへの参照があります。
[Required]
属性を Instructor
ナビゲーション プロパティに適用することができます。
[Required]
public Instructor Instructor { get; set; }
上のコードは、関連する講師が存在する必要があることを指定します。 InstructorID
外部キー (PK でもある) が 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; }
}
}
Course
エンティティには外部キー (FK) プロパティ DepartmentID
があります。 DepartmentID
は関連する Department
エンティティを指します。 Course
エンティティには Department
ナビゲーション プロパティがあります。
EF Core では、モデルに関連エンティティのナビゲーション プロパティがある場合、データ モデルの FK プロパティは必要ありません。
EF Core は、必要に応じて、データベースで自動的に FK を作成します。 EF Core は、自動的に作成された FK に対して、シャドウ プロパティを作成します。 データ モデルに FK がある場合は、更新をより簡単かつ効率的に行うことができます。 たとえば、FK プロパティ DepartmentID
が含まれていない モデルがあるとします。 Course エンティティが編集用にフェッチされた場合は、次のようになります。
Department
エンティティは、明示的に読み込まれない場合、null となります。- Course エンティティを更新するには、
Department
エンティティを最初にフェッチする必要があります。
FK エンティティ DepartmentID
がデータ モデルに含まれている場合は、更新前に Department
エンティティをフェッチする必要はありません。
DatabaseGenerated 属性
[DatabaseGenerated(DatabaseGeneratedOption.None)]
属性では、PK をデータベースで生成するのではなく、アプリケーションで提供するように指定します。
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
既定では、EF Core では PK 値が DB によって生成されるものと想定されています。 通常は、PK 値を DB で生成するのが最適です。 Course
エンティティの場合、PK はユーザーが指定します。 たとえば、数学科の場合は 1000 シリーズ、英文科の場合は 2000 シリーズなどのコース番号となります。
DatabaseGenerated
属性は、既定値を生成する場合にも使用できます。 たとえば、DB では、行が作成または更新された日付を記録するための日付フィールドを自動的に生成できます。 詳細については、「生成される値」を参照してください。
外部キー プロパティとナビゲーション プロパティ
Course
エンティティの外部キー (FK) プロパティとナビゲーション プロパティには、以下のリレーションシップが反映されます。
コースが 1 つの学科に割り当てられています。したがって、DepartmentID
FK と Department
ナビゲーション プロパティがあります。
public int DepartmentID { get; set; }
public Department Department { get; set; }
コースには任意の数の学生が登録できるため、Enrollments
ナビゲーション プロパティはコレクションとなります。
public ICollection<Enrollment> Enrollments { get; set; }
コースは複数の講師が担当する場合があるため、CourseAssignments
ナビゲーション プロパティはコレクションとなります。
public ICollection<CourseAssignment> CourseAssignments { get; set; }
CourseAssignment
については、後で説明します。
Department エンティティを作成する
次のコードを使用して 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 データ型のマッピングを変更するために使用されます。 Budget
列は、次のように、DB の SQL Server の money 型を使用して定義されます。
[Column(TypeName="money")]
public decimal Budget { get; set; }
通常、列マッピングは必要ありません。 通常、EF Core はプロパティの CLR 型に基づいて、適切な SQL Server のデータ型を選択します。 CLR decimal
型は SQL Server の decimal
型にマップされます。 Budget
は通貨用であり、通貨には money データ型がより適しています。
外部キー プロパティとナビゲーション プロパティ
FK およびナビゲーション プロパティには、次のリレーションシップが反映されます。
- 学科には管理者が存在する場合とそうでない場合があります。
- 管理者は常に講師です。 したがって、
InstructorID
プロパティはInstructor
エンティティに対する FK として含まれます。
ナビゲーション プロパティは Administrator
という名前ですが、Instructor
エンティティを保持します。
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
上のコードの疑問符 (?) は、プロパティが null 許容であることを示します。
学科には複数のコースがある場合があるため、Courses ナビゲーション プロパティがあります。
public ICollection<Course> Courses { get; set; }
注: 規則により、EF Core では、null 非許容の FK と多対多リレーションシップに対して連鎖削除が有効になります。 連鎖削除では、循環連鎖削除規則が適用される可能性があります。 循環連鎖削除規則が適用されると、移行の追加時に例外が発生します。
たとえば、Department.InstructorID
プロパティが null 許容として定義されなかった場合、次のようになります。
EF Core は、講師が削除されたときに学科を削除するように連鎖削除規則を構成します。
講師が削除されたときに学科を削除するのは、意図した動作ではありません。
次の fluent API で、連鎖の代わりに制限規則を設定します。
modelBuilder.Entity<Department>() .HasOne(d => d.Administrator) .WithMany() .OnDelete(DeleteBehavior.Restrict)
上のコードでは、学科と講師のリレーションシップの連鎖削除が無効になります。
Enrollment エンティティを更新する
1 件の登録レコードは、1 人の学生が受講する 1 つのコースに対するものです。
次のコードを使用して 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; }
}
}
外部キー プロパティとナビゲーション プロパティ
FK プロパティとナビゲーション プロパティには、次のリレーションシップが反映されます。
登録レコードは 1 つのコースに対するものであるため、CourseID
FK プロパティと Course
ナビゲーション プロパティがあります。
public int CourseID { get; set; }
public Course Course { get; set; }
登録レコードは 1 人の学生に対するものであるため、StudentID
FK プロパティと Student
ナビゲーション プロパティがあります。
public int StudentID { get; set; }
public Student Student { get; set; }
多対多リレーションシップ
Student
エンティティと Course
エンティティの間には多対多リレーションシップがあります。 Enrollment
エンティティは、データベースでペイロードがある多対多結合テーブルとして機能します。 "ペイロードがある" とは、Enrollment
テーブルに、結合テーブルの FK 以外に追加データが含まれていることを意味します (ここでは PK と Grade
)。
次の図は、エンティティ図でこれらのリレーションシップがどのようになるかを示しています (この図は、EF 6.x 用の EF Power Tools を使用して生成されたものです。このチュートリアルでは図は作成しません)。
各リレーションシップ線の一方の端に 1 が、もう一方の端にアスタリスク (*) があり、1 対多リレーションシップであることを示しています。
Enrollment
テーブルに成績情報が含まれていない場合、含める必要があるのは 2 つの FK (CourseID
と StudentID
) のみです。 ペイロードがない多対多結合テーブルは純粋結合テーブル (PJT) と呼ばれる場合があります。
Instructor
および Course
エンティティには、純粋結合テーブルを使用する多対多リレーションシップがあります。
注: EF 6.x では多対多リレーションシップの暗黙の結合テーブルがサポートされますが、EF Core ではサポートされません。 詳細については、EF Core 2.0 での多対多リレーションシップに関するページを参照してください。
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; }
}
}
講師対コース
講師対コースの多対多リレーションシップは、
- エンティティ セットで表される必要がある結合テーブルが必要です。
- 純粋結合テーブル (ペイロードがないテーブル) です。
結合エンティティには EntityName1EntityName2
という名前が付けるのが一般的です。 たとえば、このパターンを使用する講師対コースの結合テーブルは CourseInstructor
となります。 ただし、リレーションシップを説明する名前を使用することをお勧めします。
データ モデルは始めは単純なものであっても大きくなります。 ペイロードがない結合 (PJT) は頻繁に進化し、ペイロードが含まれるようになります。 最初にわかりやすいエンティティ名を付けておけば、結合テーブルが変更されたときに名前を変更する必要はありません。 結合エンティティでは、ビジネス ドメインに独自の自然な (場合によっては 1 単語の) 名前を指定することが理想的です。 たとえば、Books と Customers は Ratings という結合エンティティでリンクできます。 講師対コースの多対多リレーションシップの場合、CourseAssignment
は CourseInstructor
より優先されます。
複合キー
FK は null 非許容です。 CourseAssignment
の 2 つの FK (InstructorID
と CourseID
) を組み合わせて使用し、CourseAssignment
テーブルの各行を一意に識別します。 CourseAssignment
には専用の PK は必要ありません。 InstructorID
および CourseID
プロパティは複合 PK として機能します。 EF Core に複合 PK を指定する唯一の方法は、fluent API を使用することです。 次のセクションでは、複合 PK の構成方法を示します。
複合キーでは、必ず次のようになります。
- 1 つのコースに対して複数の行が許可される。
- 1 人の講師に対して複数の行が許可される。
- 同じ講師とコースに対して複数の行が許可されない。
Enrollment
結合エンティティでは独自の PK を定義するため、このような重複が考えられます。 このような重複を防ぐには、次のようにします。
- FK フィールドに一意のインデックスを追加する。または
CourseAssignment
と同様の複合主キーを使用して、Enrollment
を構成する。 詳細については、「インデックス」を参照してください。
DB コンテキストを更新する
次の強調表示されたコードを Data/SchoolContext.cs
に追加します。
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Models
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollment { get; set; }
public DbSet<Student> Student { 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
エンティティの複合 PK が構成されます。
属性の代わりに fluent API を使用する
上のコードの OnModelCreating
メソッドでは、fluent API を使用して EF Core の動作を構成します。 API は "fluent" と呼ばれます。これは、多くの場合、一連のメソッド呼び出しを単一のステートメントにまとめて使用されるためです。 次のコードは fluent API の例です。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
このチュートリアルでは、属性で実行できない DB マッピングの場合にのみ、fluent API を使用します。 ただし、fluent API では、属性で実行できる書式設定、検証、マッピング規則のほとんどを指定できます。
MinimumLength
などの一部の属性は fluent API で適用できません。 MinimumLength
ではスキーマを変更せず、最小長の検証規則のみを適用します。
一部の開発者は、エンティティ クラスを "クリーン" な状態に保つために、fluent API のみを使用することを好みます。属性と fluent API を混在させることができます。 (複合 PK を指定して) fluent API でのみ実行できる構成がいくつかあります。 属性 (MinimumLength
) でのみ実行できる構成もいくつかあります。 次のように、fluent API または属性を使用することをお勧めします。
- これら 2 つの方法のいずれかを選択する。
- できるだけ一貫性を保つために選択した方法を使用する。
このチュートリアルで使用する属性のいくつかは、次の用途に使用されます。
- 検証のみ (
MinimumLength
など)。 - EF Core 構成のみ (
HasKey
など)。 - 検証と EF Core の構成 (
[StringLength(50)]
など)。
属性と fluent API の詳細については、「構成の方法」を参照してください。
リレーションシップを示すエンティティ図
次の図では、完成した School モデルに対して EF Power Tools で作成される図を示します。
上の図には以下が示されています。
- いくつかの一対多リレーションシップの線 (1 対 *)。
Instructor
エンティティとOfficeAssignment
エンティティの間の一対ゼロまたは一対一リレーションシップの線 (1 対 0..1)。Instructor
エンティティとDepartment
エンティティの間のゼロ対一またはゼロ対多リレーションシップの線 (1 対 0..1)。
テスト データで DB をシードする
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.Student.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.Student.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.Enrollment.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollment.Add(e);
}
}
context.SaveChanges();
}
}
}
上のコードでは、新しいエンティティのシード データが提供されます。 このコードのほとんどで新しいエンティティ オブジェクトが作成され、サンプル データが読み込まれます。 サンプル データはテストに使用されます。 多対多結合テーブルがシードされる方法の例については、Enrollments
と CourseAssignments
を参照してください。
移行を追加する
プロジェクトをビルドします。
Add-Migration 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
コマンドを実行すると、次のエラーが生成されます。
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.
移行を適用する
既存のデータベースができたので、将来の変更を適用する方法について検討する必要があります。 このチュートリアルでは、2 つの方法を示します。
- データベースを削除して再作成する
- 移行を既存のデータベースに適用する。 この方法はより複雑で時間がかかりますが、実際の運用環境では推奨される方法です。 注:これは、チュートリアルのオプションのセクションです。 削除と再作成の手順を行い、このセクションはスキップしてもかまいません。 このセクションの手順に従う場合は、削除と再作成の手順を行わないでください。
データベースを削除して再作成する
更新された DbInitializer
のコードでは、新しいエンティティのシード データを追加します。 EF Core に新しい DB を強制的に作成させるには、DB を削除して更新します。
パッケージ マネージャー コンソール (PMC) で、次のコマンドを実行します。
Drop-Database
Update-Database
PMC から Get-Help about_EntityFrameworkCore
を実行してヘルプ情報を入手します。
アプリを実行します。 アプリを実行すると DbInitializer.Initialize
メソッドが実行されます。 DbInitializer.Initialize
は新しい DB を設定します。
SSOX で DB を開きます。
- SSOX が既に開いている場合は、 [更新] ボタンをクリックします。
- [Tables](テーブル) ノードを展開します。 作成されたテーブルが表示されます。
CourseAssignment テーブルを確認します。
- CourseAssignment テーブルを右クリックして、 [データの表示] を選択します。
- CourseAssignment テーブルにデータが含まれていることを確認します。
移行を既存のデータベースに適用する
このセクションは省略可能です。 以下の手順は、前の「データベースを削除して再作成する」セクションをスキップした場合にのみ使用できます。
既存のデータで移行が実行されている場合、既存のデータでは満たされない FK 制約が存在する可能性があります。 運用データを使用する場合は、既存のデータを移行するための手順を実行する必要があります。 このセクションでは、FK 制約違反の修正例を示します。 これらのコードをバックアップせずに変更しないでください。 前のセクションを完了し、データベースを更新した場合は、これらのコードを変更しないでください。
{timestamp}_ComplexDataModel.cs
ファイルには、次のコードが含まれています。
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
type: "int",
nullable: false,
defaultValue: 0);
上のコードでは、null 非許容の DepartmentID
FK が Course
テーブルに追加されます。 前のチュートリアルの DB には Course
の行が含まれるため、テーブルを移行して更新することはできません。
既存のデータを使用して ComplexDataModel
の移行を実行するには、次のようにします。
- コードを変更して、新しい列 (
DepartmentID
) に既定値を設定します。 - "Temp" という名前の偽の学科を作成し、既定の学科として機能するようにします。
外部キー制約を修正する
ComplexDataModel
クラスの Up
メソッドを更新します。
{timestamp}_ComplexDataModel.cs
ファイルを開きます。DepartmentID
列をCourse
テーブルに追加するコードの行をコメントアウトします。
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);
次の強調表示されたコードを追加します。 新しいコードが .CreateTable( name: "Department"
ブロックの後に配置されます。
migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(type: "int", nullable: true),
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(type: "datetime2", 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);
前述の変更に伴い、既存の Course
行が、ComplexDataModel
Up
メソッドの実行後に "Temp" 学科に関連付けられます。
運用アプリは次のことを行います。
- コードまたはスクリプトを組み込み、
Department
行と関連するCourse
行を新しいDepartment
行に追加します。 Course.DepartmentID
の既定値や "Temp" 学科は使用しません。
次のチュートリアルでは関連するデータについて説明します。
その他の技術情報
ASP.NET Core