パート 5、ASP.NET Core での EF Core を使用した Razor ページ - データ モデル

作成者: Tom DykstraJeremy LiknessJon P Smith

Contoso 大学 Web アプリでは、EF Core と Visual Studio を使用して Razor Pages Web アプリを作成する方法を示します。 チュートリアル シリーズについては、最初のチュートリアルを参照してください。

解決できない問題が発生した場合は、完成したアプリをダウンロードし、チュートリアルに従って作成した内容とコードを比較します。

前のチュートリアルでは、3 つのエンティティで構成された基本的なデータ モデルを使用して作業を行いました。 このチュートリアルでは、次の作業を行います。

  • エンティティとリレーションシップをさらに追加する。
  • 書式設定、検証、データベース マッピングの規則を指定して、データ モデルをカスタマイズする。

完成したデータ モデルは、次の図のようになります。

Entity diagram

次のデータベース ダイアグラムは Dataedo で作成されました。

Dataedo diagram

Dataedo でデータベース ダイアグラムを作成するには:

前の 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 テーブル デザイナーを開きます。

Students table in SSOX before migrations

上の図には 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 属性では、名前プロパティの必須フィールドを作成します。 値の型 (例: DateTimeintdouble) などの null 非許容型では、Required 属性は必要ありません。 null にできない型は自動的に必須フィールドとして扱われます。

MinimumLength を適用するには、Required 属性を MinimumLength と共に使用する必要があります。

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

MinimumLengthRequired を使用すると、空白で検証を満たすことができます。 文字列を完全に制御するには、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 テーブルを開きます。

    Students table in SSOX after migrations

    移行が適用される前の名前列の型は 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)]

CoursesOfficeAssignment プロパティはナビゲーション プロパティです。

講師は任意の数のコースを担当できるため、Courses はコレクションとして定義されます。

public ICollection<Course> Courses { get; set; }

講師は最大で 1 つのオフィスを持つことができるので、OfficeAssignment プロパティには 1 つの OfficeAssignment エンティティが保持されます。 オフィスが割り当てられていない場合、OfficeAssignment は null です。

public OfficeAssignment OfficeAssignment { get; set; }

OfficeAssignment エンティティ

OfficeAssignment entity

次のコードを使用して 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] 属性は、プロパティ名が classnameIDID 以外である場合に、主キー (PK) としてプロパティを識別するために使用されます。

Instructor エンティティと OfficeAssignment エンティティの間には一対ゼロまたは一対一のリレーションシップがあります。 オフィスが割り当てられている講師についてのみ、オフィス割り当てが存在します。 OfficeAssignment PK は、Instructor エンティティに対する外部キー (FK) でもあります。 一対ゼロまたは一対一のリレーションシップは、あるテーブルの PK が、別のテーブルの PK と FK の両方である場合に発生します。

InstructorID が ID または classnameID の名前付け規則に従っていないため、EF Core では、InstructorIDOfficeAssignment の 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 つのコースに対するものです。

Enrollment entity

次のコードを使用して 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 を使用して生成されたものです。このチュートリアルでは図は作成しません)。

Student-Course many to many relationship

各リレーションシップ線の一方の端に 1 が、もう一方の端にアスタリスク (*) があり、1 対多リレーションシップであることを示しています。

Enrollment テーブルに成績情報が含まれていない場合、含める必要があるのは 2 つの FK (CourseIDStudentID) のみです。 ペイロードがない多対多結合テーブルは純粋結合テーブル (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 つの方法でデータベースを変更できます。

どちらの選択肢も SQL Server で機能します。 移行適用方法はより複雑で時間がかかりますが、実際の運用環境では推奨される方法です。

データベースを削除して再作成する

EF Core に新しいデータベースを強制的に作成させるには、データベースを削除して更新します。

  • Migrations フォルダーを削除します。
  • パッケージ マネージャー コンソール (PMC) で、次のコマンドを実行します。
Drop-Database
Add-Migration InitialCreate
Update-Database

アプリを実行します。 アプリを実行すると DbInitializer.Initialize メソッドが実行されます。 DbInitializer.Initialize では、新しいデータベースが設定されます。

SSOX でデータベースを開きます。

  • SSOX が既に開いている場合は、 [更新] ボタンをクリックします。
  • [Tables](テーブル) ノードを展開します。 作成されたテーブルが表示されます。

次の手順

次の 2 つのチュートリアルでは、関連データを読み取って更新する方法について説明します。

前のチュートリアルでは、3 つのエンティティで構成された基本的なデータ モデルを使用して作業を行いました。 このチュートリアルでは、次の作業を行います。

  • エンティティとリレーションシップをさらに追加する。
  • 書式設定、検証、データベース マッピングの規則を指定して、データ モデルをカスタマイズする。

完成したデータ モデルは、次の図のようになります。

Entity diagram

Student エンティティ

Student entity

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 テーブル デザイナーを開きます。

Students table in SSOX before migrations

上の図には 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 属性では、名前プロパティの必須フィールドを作成します。 値の型 (例: DateTimeintdouble) などの null 非許容型では、Required 属性は必要ありません。 null にできない型は自動的に必須フィールドとして扱われます。

MinimumLength を適用するには、Required 属性を MinimumLength と共に使用する必要があります。

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

MinimumLengthRequired を使用すると、空白で検証を満たすことができます。 文字列を完全に制御するには、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 テーブルを開きます。

    Students table in SSOX after migrations

    移行が適用される前の名前列の型は nvarchar(MAX) でした。 現在の名前列は nvarchar(50) です。 列名は FirstMidName から FirstName に変わりました。

  • アプリを実行して [Students] ページに移動します。
  • 日付と共に時刻が入力および表示されていないことに注意してください。
  • [新規作成] を選択し、50 文字を超える名前を入力してみます。

Note

次のセクションでは、いくつかのステージでアプリをビルドします。その場合、コンパイラ エラーが生成されます。 手順では、アプリをビルドするタイミングを指定します。

Instructor エンティティ

Instructor entity

次のコードを使用して 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)]

CourseAssignmentsOfficeAssignment プロパティはナビゲーション プロパティです。

講師は任意の数のコースを担当できるため、CourseAssignments はコレクションとして定義されます。

public ICollection<CourseAssignment> CourseAssignments { get; set; }

講師は最大で 1 つのオフィスを持つことができるので、OfficeAssignment プロパティには 1 つの OfficeAssignment エンティティが保持されます。 オフィスが割り当てられていない場合、OfficeAssignment は null です。

public OfficeAssignment OfficeAssignment { get; set; }

OfficeAssignment エンティティ

OfficeAssignment entity

次のコードを使用して 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 では、InstructorIDOfficeAssignment の 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 エンティティ

Course entity

次のコードを使用して 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 エンティティ

Department entity

次のコードを使用して 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 つのコースに対するものです。

Enrollment entity

次のコードを使用して 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 を使用して生成されたものです。このチュートリアルでは図は作成しません)。

Student-Course many to many relationship

各リレーションシップ線の一方の端に 1 が、もう一方の端にアスタリスク (*) があり、1 対多リレーションシップであることを示しています。

Enrollment テーブルに成績情報が含まれていない場合、含める必要があるのは 2 つの FK (CourseIDStudentID) のみです。 ペイロードがない多対多結合テーブルは純粋結合テーブル (PJT) と呼ばれる場合があります。

Instructor および Course エンティティには、純粋結合テーブルを使用する多対多リレーションシップがあります。

注: EF 6.x では多対多リレーションシップの暗黙の結合テーブルがサポートされますが、EF Core ではサポートされません。 詳細については、EF Core 2.0 での多対多リレーションシップに関するページを参照してください。

CourseAssignment エンティティ

CourseAssignment entity

次のコードを使用して 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 です。

Instructor-to-Courses m:M

結合エンティティには EntityName1EntityName2 という名前が付けるのが一般的です。 たとえば、このパターンを使用する講師対コースの結合テーブルは CourseInstructor になります。 ただし、リレーションシップを説明する名前を使用することをお勧めします。

データ モデルは始めは単純なものであっても大きくなります。 ペイロードがない結合テーブル (PJT) は、頻繁に更新されてペイロードが追加されます。 最初にわかりやすいエンティティ名を付けておけば、結合テーブルが変更されたときに名前を変更する必要はありません。 結合エンティティでは、ビジネス ドメインに独自の自然な (場合によっては 1 単語の) 名前を指定することが理想的です。 たとえば、Books と Customers は Ratings という結合エンティティでリンクできます。 講師対コースの多対多リレーションシップの場合、CourseAssignmentCourseInstructor より優先されます。

複合キー

CourseAssignment の 2 つの FK (InstructorIDCourseID) を組み合わせて使用し、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 で作成される図を示します。

Entity diagram

上の図には以下が示されています。

  • いくつかの一対多リレーションシップの線 (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();
        }
    }
}

上のコードでは、新しいエンティティのシード データが提供されます。 このコードのほとんどで新しいエンティティ オブジェクトが作成され、サンプル データが読み込まれます。 サンプル データはテストに使用されます。 多対多結合テーブルがシードされる方法の例については、EnrollmentsCourseAssignments を参照してください。

移行を追加する

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

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 つの方法を示します。

どちらの選択肢も SQL Server で機能します。 移行適用方法はより複雑で時間がかかりますが、実際の運用環境では推奨される方法です。

データベースを削除して再作成する

SQL Server を使用していて、次のセクションの移行適用方法を実行する場合は、このセクションをスキップします

EF Core に新しいデータベースを強制的に作成させるには、データベースを削除して更新します。

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

    Drop-Database
    
  • Migrations フォルダーを削除し、次のコマンドを実行します。

    Add-Migration InitialCreate
    Update-Database
    

アプリを実行します。 アプリを実行すると DbInitializer.Initialize メソッドが実行されます。 DbInitializer.Initialize では、新しいデータベースが設定されます。

SSOX でデータベースを開きます。

  • SSOX が既に開いている場合は、 [更新] ボタンをクリックします。

  • [Tables](テーブル) ノードを展開します。 作成されたテーブルが表示されます。

    Tables in SSOX

  • CourseAssignment テーブルを確認します。

    • CourseAssignment テーブルを右クリックして、 [データの表示] を選択します。
    • CourseAssignment テーブルにデータが含まれていることを確認します。

    CourseAssignment data in SSOX

移行を適用する

このセクションは省略可能です。 以下の手順は、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 つのエンティティで構成された基本的なデータ モデルを使用して作業を行いました。 このチュートリアルでは、次の作業を行います。

  • エンティティとリレーションシップをさらに追加する。
  • 書式設定、検証、データベース マッピングの規則を指定して、データ モデルをカスタマイズする。

完成したデータ モデルのエンティティ クラスは、次の図のようになります。

Entity diagram

解決できない問題が発生した場合は、完成したアプリをダウンロードしてください。

属性を使用してデータ モデルをカスタマイズする

このセクションでは、属性を使用してデータ モデルをカスタマイズします。

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 モデルを使用するすべてのビューに、時刻なしの日付が表示されます。

Students index page showing dates without times

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 文字を超える名前を入力します。
  • [作成] を選択すると、クライアント側の検証でエラー メッセージが表示されます。

Students index page showing string length errors

SQL Server オブジェクト エクスプローラー (SSOX) で、Student テーブルをダブルクリックして、Student テーブル デザイナーを開きます。

Students table in SSOX before migrations

上の図には 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.FirstMidNameStudent テーブルの 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 テーブルを開きます。

Students table in SSOX after migrations

移行が適用される前の名前列の型は nvarchar(MAX) でした。 現在の名前列は nvarchar(50) です。 列名は FirstMidName から FirstName に変わりました。

Note

次のセクションでは、いくつかのステージでアプリをビルドします。その場合、コンパイラ エラーが生成されます。 手順では、アプリをビルドするタイミングを指定します。

Student エンティティの更新

Student entity

次のコードを使用して 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 属性では、名前プロパティの必須フィールドを作成します。 値の型 (DateTimeintdouble など) などの 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 エンティティを作成する

Instructor entity

次のコードを使用して 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 ナビゲーション プロパティ

CourseAssignmentsOfficeAssignment プロパティはナビゲーション プロパティです。

講師は任意の数のコースを担当できるため、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 エンティティを作成する

OfficeAssignment entity

次のコードを使用して 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 エンティティを変更する

Course entity

次のコードを使用して 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 エンティティを作成する

Department entity

次のコードを使用して 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 つのコースに対するものです。

Enrollment entity

次のコードを使用して 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 を使用して生成されたものです。このチュートリアルでは図は作成しません)。

Student-Course many to many relationship

各リレーションシップ線の一方の端に 1 が、もう一方の端にアスタリスク (*) があり、1 対多リレーションシップであることを示しています。

Enrollment テーブルに成績情報が含まれていない場合、含める必要があるのは 2 つの FK (CourseIDStudentID) のみです。 ペイロードがない多対多結合テーブルは純粋結合テーブル (PJT) と呼ばれる場合があります。

Instructor および Course エンティティには、純粋結合テーブルを使用する多対多リレーションシップがあります。

注: EF 6.x では多対多リレーションシップの暗黙の結合テーブルがサポートされますが、EF Core ではサポートされません。 詳細については、EF Core 2.0 での多対多リレーションシップに関するページを参照してください。

CourseAssignment エンティティ

CourseAssignment entity

次のコードを使用して 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 m:M

講師対コースの多対多リレーションシップは、

  • エンティティ セットで表される必要がある結合テーブルが必要です。
  • 純粋結合テーブル (ペイロードがないテーブル) です。

結合エンティティには EntityName1EntityName2 という名前が付けるのが一般的です。 たとえば、このパターンを使用する講師対コースの結合テーブルは CourseInstructor となります。 ただし、リレーションシップを説明する名前を使用することをお勧めします。

データ モデルは始めは単純なものであっても大きくなります。 ペイロードがない結合 (PJT) は頻繁に進化し、ペイロードが含まれるようになります。 最初にわかりやすいエンティティ名を付けておけば、結合テーブルが変更されたときに名前を変更する必要はありません。 結合エンティティでは、ビジネス ドメインに独自の自然な (場合によっては 1 単語の) 名前を指定することが理想的です。 たとえば、Books と Customers は Ratings という結合エンティティでリンクできます。 講師対コースの多対多リレーションシップの場合、CourseAssignmentCourseInstructor より優先されます。

複合キー

FK は null 非許容です。 CourseAssignment の 2 つの FK (InstructorIDCourseID) を組み合わせて使用し、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 で作成される図を示します。

Entity diagram

上の図には以下が示されています。

  • いくつかの一対多リレーションシップの線 (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();
        }
    }
}

上のコードでは、新しいエンティティのシード データが提供されます。 このコードのほとんどで新しいエンティティ オブジェクトが作成され、サンプル データが読み込まれます。 サンプル データはテストに使用されます。 多対多結合テーブルがシードされる方法の例については、EnrollmentsCourseAssignments を参照してください。

移行を追加する

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

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](テーブル) ノードを展開します。 作成されたテーブルが表示されます。

Tables in SSOX

CourseAssignment テーブルを確認します。

  • CourseAssignment テーブルを右クリックして、 [データの表示] を選択します。
  • CourseAssignment テーブルにデータが含まれていることを確認します。

CourseAssignment data in SSOX

移行を既存のデータベースに適用する

このセクションは省略可能です。 以下の手順は、前の「データベースを削除して再作成する」セクションをスキップした場合にのみ使用できます。

既存のデータで移行が実行されている場合、既存のデータでは満たされない 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 行が、ComplexDataModelUp メソッドの実行後に "Temp" 学科に関連付けられます。

運用アプリは次のことを行います。

  • コードまたはスクリプトを組み込み、Department 行と関連する Course 行を新しい Department 行に追加します。
  • Course.DepartmentID の既定値や "Temp" 学科は使用しません。

次のチュートリアルでは関連するデータについて説明します。

その他の技術情報