次の方法で共有


チュートリアル: ASP.NET MVC アプリで使用するより複雑なデータ モデルを作成する

ここまでのチュートリアルでは、3 つのエンティティで構成される単純なデータ モデルに関する作業を行いました。 このチュートリアルでは、エンティティとリレーションシップをさらに追加し、書式設定、検証、データベース マッピングの規則を指定してデータ モデルをカスタマイズします。 この記事では、データ モデルをカスタマイズする 2 つの方法について説明します。これらは、エンティティ クラスに属性を追加する方法と、データベース コンテキスト クラスにコードを追加する方法です。

完了すると、エンティティ クラスは、以下の図のように完成したデータ モデルを構成します。

School_class_diagram

このチュートリアルでは、次の作業を行いました。

  • データ モデルをカスタマイズする
  • Student エンティティを更新する
  • Instructor エンティティを作成する
  • OfficeAssignment エンティティを作成する
  • Course エンティティを変更する
  • Department エンティティを作成する
  • Enrollment エンティティを変更する
  • データベース コンテキストにコードを追加する
  • テスト データを使ってデータベースをシードする
  • 移行を追加する
  • データベースを更新する

前提条件

データ モデルをカスタマイズする

このセクションでは、書式設定、検証、データベース マッピング規則を指定する属性を使用して、データ モデルをカスタマイズする方法を示します。 その後のいくつかのセクションで、既に作成したクラスに属性を追加し、モデルの残りのエンティティ型に対する新しいクラスを作成して、完全な School データ モデルを作成します。

DataType 属性

学生の登録日について、すべての Web ページでは現在、日付と共に時刻が表示されていますが、このフィールドでは日付が重要になります。 データ注釈属性を使用すれば、1 つのコードを変更するだけで、データを表示するすべてのビューの表示形式を修正できます。 その方法例を表示するには、Student クラスの EnrollmentDate プロパティに属性を追加します。

Models\Student.cs で、次の例で示すように、System.ComponentModel.DataAnnotations 名前空間の using ステートメントを追加し、DataTypeDisplayFormat 属性を EnrollmentDate プロパティに追加します。

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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

DataType 属性は、データベースの組み込み型より具体的なデータ型を指定するために使われます。 この例では、追跡する必要があるのは、日付と時刻ではなく、日付のみです。 DataType 列挙型は、Date、Time、PhoneNumber、Currency、EmailAddress など、多くのデータ型に対応しています。 また、DataType 属性を使用して、アプリケーションで型固有の機能を自動的に提供することもできます。 たとえば、DataType.EmailAddress に対して mailto: リンクを作成したり、HTML5 をサポートするブラウザーで DataType.Date に日付セレクターを提供したりできます。 DataType 属性は、HTML 5 ブラウザーが認識できる HTML 5 の data- ("データ ダッシュ" と読みます) 属性を出力します。 DataType 属性では、どのような検証も提供されません。

DataType.Date は、表示される日付の書式を指定しません。 既定では、データ フィールドはサーバーの CultureInfo に基づく既定の書式に従って表示されます。

DisplayFormat 属性は、日付の書式を明示的に指定するために使用されます。

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

ApplyFormatInEditMode 設定は、値が編集用のテキスト ボックスに表示されるときも、指定されている書式設定を適用する必要があることを指定します。 (フィールドによっては適用したくないこともあります。たとえば、通貨値では、編集用テキスト ボックスには通貨記号が必要でない場合があります)。

DisplayFormat 属性を単独で使用できますが、一般に DataType 属性も使うことをお勧めします。 DataType 属性では、画面でのレンダリング方法ではなく、データの "セマンティクス" が伝達され、DisplayFormat では得られない次のような利点があります。

  • ブラウザーは HTML5 機能を有効にすることができます (たとえば、カレンダー コントロール、ロケールに適した通貨記号、メール リンク、一部のクライアント側の入力検証を表示するときなど)。
  • 既定で、ロケールに基づく正しい書式を使ってデータがブラウザーにレンダリングされます。
  • DataType 属性を使うと、MVC でデータをレンダリングするための正しいフィールド テンプレートを選択できるようになります (DisplayFormat では文字列テンプレートが使われます)。 詳しくは、Brad Wilson の ASP.NET MVC 2 テンプレートに関するページをご覧ください。 (この記事は、MVC 2 に関して書かれていますが、ASP.NET MVC の現在のバージョンにも当てはまります)。

日付フィールドで DataType 属性を使う場合は、Chrome ブラウザーでフィールドが正しくレンダリングされるようにするには、DisplayFormat 属性も指定する必要があります。 詳しくは、StackOverflow のこちらのスレッドをご覧ください。

MVC で他の日付形式を処理する方法の詳細については、MVC 5 の概要: Edit メソッドと Edit ビューの確認に関するページにアクセスし、「internationalization」 (国際化) と検索してください。

Student インデックス ページをもう一度実行して、登録日の時刻が表示されなくなることに注目してください。 Student モデルを使うどのビューでも同様です。

Students_index_page_with_formatted_date

StringLength 属性

属性を使用して、データ検証規則と検証エラー メッセージを指定することもできます。 StringLength 属性はデータベースでの最大長を設定し、クライアント側とサーバー側の検証を ASP.NET MVC に提供します。 この属性で最小長を指定することもできますが、最小値はデータベース スキーマに影響しません。

たとえば、ユーザーが 50 文字を超える名前を入力しないようにする必要があるとします。 この制限を追加するには、次の例で示すように、StringLength 属性を LastNameFirstMidName プロパティに追加します。

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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

StringLength 属性により、ユーザーが名前に空白を入力できなくなることはありません。 入力に制限を適用するには、RegularExpression 属性を使用できます。 たとえば、次のコードでは、最初の文字を大文字にし、残りの文字をアルファベット順にすることを要求しています。

[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]

MaxLength 属性は、StringLength 属性と同様の機能を提供しますが、クライアント側の検証は行われません。

アプリケーションを実行して、[Students] タブをクリックします。次のエラーが表示されます。

データベースの作成後、'SchoolContext' コンテキストの背後にあるモデルが変更されました。 Code First Migrations を使用したデータベースの更新を検討してください (https://go.microsoft.com/fwlink/?LinkId=238269)。

データベース スキーマでの変更が必要な方法でデータベース モデルが変更され、Entity Framework がそれを検出しました。 UI を使ってデータベースに追加したデータを失わずにスキーマを更新するには、移行を使います。 Seed メソッドによって作成されたデータを変更した場合、Seed メソッドで使っている AddOrUpdate メソッドのために、それは元の状態に戻されます。 (AddOrUpdate は、データベース用語の "アップサート" 操作に相当します)。

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

add-migration MaxLengthOnNames
update-database

add-migration コマンドにより、timeStamp<>_MaxLengthOnNames.cs という名前のファイルが作成されます。 このファイルには、現在のデータ モデルと一致するようにデータベースを更新する Up メソッドのコードが含まれます。 update-database コマンドでそのコードが実行されました。

移行ファイル名の前に付けられるタイムスタンプは、移行を並べ替えるために Entity Framework によって使われます。 update-database コマンドを実行する前に、複数の移行を作成することができます。その後、すべての移行は作成順に適用されます。

[Create] ページを実行し、50 文字を超える名前を入力します。 [作成] をクリックすると、クライアント側の検証で The field LastName must be a string with a maximum length of 50. (LastName フィールドは、最大長 50 文字の文字列である必要があります。) というエラー メッセージが表示されます。

Column 属性

属性を使用して、データベースへのクラスとプロパティのマッピング方法を制御することもできます。 フィールドにミドル ネームも含まれている場合があるため、名フィールドに対して FirstMidName という名前を使用したとします。 ただし、データベース列は FirstName という名前にする必要があります。これは、データベースに対するアドホック クエリを記述するユーザーがその名前に慣れているためです。 このマッピングを作成する場合、Column 属性を使用できます。

Column 属性は、データベースの作成時に、FirstMidName プロパティにマップする Student テーブルの列が FirstName という名前になるように指定します。 つまり、コードが Student.FirstMidName を参照したときに、データが Student テーブルの FirstName 列から取り込まれるか、更新されます。 列名を指定しない場合は、プロパティ名と同じ名前が付けられます。

次の強調されたコードで示されているように、Student.cs ファイルで System.ComponentModel.DataAnnotations.Schemausing ステートメントを追加し、列名属性を FirstMidName プロパティに追加します。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]       
        public string LastName { get; set; }
        [StringLength(50, 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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

Column 属性を追加すると、SchoolContext の基になっているモデルが変更されるため、データベースと一致しなくなります。 PMC で次のコマンドを入力して、別の移行を作成します。

add-migration ColumnFirstName
update-database

サーバー エクスプローラーStudent テーブルをダブルクリックして、Student テーブル デザイナーを開きます。

次に示す図は、最初の 2 つの移行を適用する前の元の列名です。 FirstMidName から FirstName への列名の変更だけでなく、2 つの名前列が MAX 長から 50 文字に変更されています。

2 つの Student テーブルの名前とデータ型の違いを示す 2 つのスクリーンショット。

このチュートリアルで後ほど説明するように、Fluent API を使ってデータベースのマッピングを変更することもできます。

Note

次のセクションですべてのエンティティ クラスの作成を完了する前にコンパイルしようとすると、コンパイラ エラーが発生する可能性があります。

Student エンティティを更新する

Models\Student.cs で、前に追加したコードを次のコードに置き換えます。 変更が強調表示されます。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

Required 属性

Required 属性は、name プロパティを必須フィールドにします。 Required attribute は、DateTime、int、double、float などの値の型には必要ありません。 値の型には null 値を割り当てることができないので、これらは本質的には必須フィールドとして扱われます。

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

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

MinimumLengthRequired を使用すると、空白で検証を満たすことができます。 文字列を完全に制御するには、RegularExpression 属性を使用します。

Display 属性

Display 属性は、テキスト ボックスのキャプションを、各インスタンスのプロパティ名 (単語を区切るスペースがない) の代わりに、"First Name"、"Last Name"、"Full Name"、"Enrollment Date" にするよう指定します。

FullName 集計プロパティ

FullName は集計プロパティであり、2 つの別のプロパティを連結して作成される値を返します。 したがって、get アクセサーのみを持ち、データベースに FullName 列は生成されません。

Instructor エンティティを作成する

Models\Instructor.cs を作成し、テンプレート コードを次のコードに置き換えます。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

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

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

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

Student エンティティと Instructor エンティティのいくつかのプロパティが同じであることに注目してください。 このシリーズの後半の継承の実装に関するチュートリアルでは、冗長さをなくすため、このコードをリファクタリングします。

複数の属性を 1 行にまとめて、Instructor クラスを次のように記述することもできます。

public class Instructor
{
   public int ID { get; set; }

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

   [Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)]
   public string FirstMidName { get; set; }

   [DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
   public DateTime HireDate { get; set; }

   [Display(Name = "Full Name")]
   public string FullName
   {
      get { return LastName + ", " + FirstMidName; }
   }

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

Courses と OfficeAssignment ナビゲーション プロパティ

CoursesOfficeAssignment プロパティはナビゲーション プロパティです。 前に説明したように、通常、これらは virtual として定義されているため、遅延読み込みと呼ばれる Entity Framework の機能を利用できます。 さらに、ナビゲーション プロパティが複数のエンティティを保持できる場合、その型は ICollection<T> インターフェイスを実装している必要があります。 たとえば、IList<T> は適格ですが、IEnumerable<T>IEnumerable<T>Add を実装していないため不適格です。

講師はコースをいくつでも担当できるため、CoursesCourse エンティティのコレクションとして定義されています。

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

このビジネス ルールでは、講師は多くても 1 つのオフィスしか持つことができないため、OfficeAssignment は 1 つの OfficeAssignment エンティティとして定義されています (オフィスが割り当てられていない場合、null になる可能性があります)。

public virtual OfficeAssignment OfficeAssignment { get; set; }

OfficeAssignment エンティティを作成する

次のコードで Models\OfficeAssignment.cs を作成します。

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        [ForeignKey("Instructor")]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public virtual Instructor Instructor { get; set; }
    }
}

プロジェクトをビルドします。これにより、変更が保存され、コンパイラがキャッチできるコピーと貼り付けのエラーを行っていないことが確認されます。

Key 属性

Instructor エンティティと OfficeAssignment エンティティの間には一対ゼロまたは一対一のリレーションシップがあります。 オフィスが割り当てられている講師についてのみ、オフィス割り当てが存在します。したがって、その主キーは Instructor エンティティに対する外部キーでもあります。 ただし、Entity Framework では、名前がIDまたは classname ID 名前付け規則に従っていないため、InstructorIDをこのエンティティの主キーとして自動的に認識することはできません。 したがって、Key 属性はキーとして識別するために使用されます。

[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }

エンティティに独自の主キーがあっても、プロパティに classnameIDID とは異なる名前を付けたい場合は、Key 属性を使うこともできます。 列は依存リレーションシップに対するものであるため、既定では EF はキーを非データベース生成として扱います。

ForeignKey 属性

2 つのエンティティの間に "一対ゼロまたは一" リレーションシップまたは "一対一" リレーションシップがある場合 (OfficeAssignmentInstructor の間など)、EF はリレーションシップのどちらの端が主で、どちらの端が従かを解決できません。 一対一のリレーションシップでは、各クラスに他のクラスへの参照ナビゲーション プロパティがあります。 ForeignKey 属性を従属クラスに適用して、リレーションシップを確立できます。 ForeignKey 属性を省略すると、移行を作成しようとしたときに次のエラーが発生します。

型 'ContosoUniversity.Models.OfficeAssignment' と 'ContosoUniversity.Models.Instructor' の間のアソシエーションのプリンシパル End を特定できません。 relationship fluent API またはデータ注釈を使用して、このアソシエーションのプリンシパル End を明示的に構成する必要があります。

このチュートリアルで後ほど、fluent API でこのリレーションシップを構成する方法を説明します。

Instructor ナビゲーション プロパティ

Instructor エンティティには null 許容の OfficeAssignment ナビゲーション プロパティがあり (講師にオフィスが割り当てられていない場合があるため)、OfficeAssignment エンティティには null 非許容の Instructor ナビゲーション プロパティがあります (講師なしではオフィス割り当てが存在できない、つまり、InstructorID が null 非許容であるため)。 Instructor エンティティに関連する OfficeAssignment エンティティがある場合、各エンティティにはそのナビゲーション プロパティでの他のものへの参照があります。

Instructor ナビゲーション プロパティで [Required] 属性を指定して、関連する講師が存在する必要があることを指定できますが、InstructorID 外部キー (このテーブルに対するキーでもある) は null 非許容であるため、そうする必要はありません。

Course エンティティを変更する

Models\Course.cs で、前に追加したコードを次のコードに置き換えます。

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
   public class Course
   {
      [DatabaseGenerated(DatabaseGeneratedOption.None)]
      [Display(Name = "Number")]
      public int CourseID { get; set; }

      [StringLength(50, MinimumLength = 3)]
      public string Title { get; set; }

      [Range(0, 5)]
      public int Credits { get; set; }

      public int DepartmentID { get; set; }

      public virtual Department Department { get; set; }
      public virtual ICollection<Enrollment> Enrollments { get; set; }
      public virtual ICollection<Instructor> Instructors { get; set; }
   }
}

Course エンティティには、関連する Department エンティティを指す外部キー プロパティ DepartmentID があり、Department ナビゲーション プロパティを持っています。 Entity Framework では、関連エンティティのナビゲーション プロパティがある場合、ユーザーがデータ モデルに外部キー プロパティを追加する必要はありません。 EFは、必要に応じて、データベースで自動的に外部キーを作成します。 ただし、データ モデルに外部キーがある場合は、更新をより簡単かつ効率的に行うことができます。 たとえば、編集する Course エンティティをフェッチするとき、Department エンティティを読み込まないとそれは null になるので、Course エンティティを更新するときは、最初に Department エンティティをフェッチする必要があります。 外部キー プロパティ DepartmentID がデータ モデルに含まれている場合は、更新前に Department エンティティをフェッチする必要はありません。

DatabaseGenerated 属性

CourseID プロパティで DatabaseGenerated 属性と None パラメーターを使うと、主キー値がデータベースによって生成されるのではなく、ユーザーによって提供されることを指定します。

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

既定では、Entity Framework は主キー値がデータベースによって生成されることを前提とします。 これはほとんどのシナリオに該当します。 ただし、Course エンティティの場合、1 つの学科に 1000 シリーズ、別の学科に 2000 シリーズといったユーザー指定のコース番号を使用します。

外部キー プロパティとナビゲーション プロパティ

Course エンティティの外部キー プロパティとナビゲーション プロパティには、以下のリレーションシップが反映されます。

  • コースは 1 つの学科に割り当てられます。したがって、前述の理由により、DepartmentID 外部キーと Department ナビゲーション プロパティが存在します。

    public int DepartmentID { get; set; }
    public virtual Department Department { get; set; }
    
  • コースには任意の数の学生が登録できるため、Enrollments ナビゲーション プロパティはコレクションとなります。

    public virtual ICollection<Enrollment> Enrollments { get; set; }
    
  • コースは複数の講師が担当する場合があるため、Instructors ナビゲーション プロパティはコレクションとなります。

    public virtual 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 virtual Instructor Administrator { get; set; }
      public virtual ICollection<Course> Courses { get; set; }
   }
}

Column 属性

これまでは、Column 属性を使って、列名のマッピングを変更しました。 Department エンティティのコードでは、Column 属性は SQL データ型のマッピングを変更するために使われているため、列はデータベースの SQL Server money 型を使って定義されます。

[Column(TypeName="money")]
public decimal Budget { get; set; }

列のマッピングは一般に必要ありません。これは、Entity Framework は、通常、プロパティに対して定義された CLR 型に基づいて、適切な SQL Server のデータ型を選ぶためです。 CLR decimal 型は SQL Server の decimal 型にマップされます。 ただし、ここでは、列に通貨額が保持されることがわかっているため、money データ型がより適しています。 CLR データ型について、およびそれらが SQL Server データ型にどのように対応するかの詳細については、「Entity Framework 用 SqlClient の型」を参照してください。

外部キー プロパティとナビゲーション プロパティ

外部キーおよびナビゲーション プロパティには、次のリレーションシップが反映されます。

  • 学科には管理者が存在する場合とそうでない場合があり、管理者は常に講師となります。 したがって、InstructorID プロパティは Instructor エンティティに対する外部キーとして含まれており、プロパティを null 許容とマークするために int 型の指定の後に疑問符が追加されます。ナビゲーション プロパティの名前は Administrator ですが、Instructor エンティティを保持します。

    public int? InstructorID { get; set; }
    public virtual Instructor Administrator { get; set; }
    
  • 1 つの学科に多くのコースがある場合があるため、Courses ナビゲーション プロパティがあります。

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

    Note

    規則により、Entity Framework では null 非許容の外部キーと多対多リレーションシップに対して連鎖削除が有効になります。 これにより、循環連鎖削除規則が適用される可能性があり、移行を追加しようとすると例外が発生します。 たとえば、Department.InstructorID プロパティを null 許容として定義しなかった場合、次の例外メッセージが表示されます: "参照型リレーションシップにより、許可されない循環参照が発生します"。ビジネス ルールで InstructorID プロパティを null 非許容にすることが求められている場合、以下の fluent API ステートメントを使用して、リレーションシップで連鎖削除を無効にする必要があります。

modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);

Enrollment エンティティを変更する

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 virtual Course Course { get; set; }
        public virtual Student Student { get; set; }
    }
}

外部キー プロパティとナビゲーション プロパティ

外部キー プロパティとナビゲーション プロパティには、次のリレーションシップが反映されます。

  • 登録レコードは単一のコースに対するものであるため、CourseID 外部キー プロパティと Course ナビゲーション プロパティがあります。

    public int CourseID { get; set; }
    public virtual Course Course { get; set; }
    
  • 登録レコードは 1 人の学生に対するものであるため、StudentID 外部キー プロパティと Student ナビゲーション プロパティがあります。

    public int StudentID { get; set; }
    public virtual Student Student { get; set; }
    

多対多リレーションシップ

Student および Course エンティティの間には多対多リレーションシップがあり、Enrollment エンティティはデータベースで "ペイロードがある" 多対多の結合テーブルとして機能します。 これは、Enrollment テーブルに統合テーブルの外部キー以外に追加データが含まれていることを意味します (この例では、主キーと Grade プロパティ)。

次の図は、エンティティ図でこれらのリレーションシップがどのようになるかを示しています (この図は、Entity Framework Power Tools を使って生成されたものです。このチュートリアルでは図は作成しません。ここでは例として使用するだけです。)

Student-Course_many-to-many_relationship

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

Enrollment テーブルに成績情報が含まれていない場合、含める必要があるのは 2 つの外部キー CourseID および StudentID のみです。 その場合、それはデータベース内の "ペイロードを持たない" 多対多結合テーブル (つまり "結合テーブルだけ") に対応しており、それに対してモデル クラスを作成する必要はまったくありません。 InstructorCourse エンティティには、その種の多対多リレーションシップがあり、ご覧のとおり、それらの間にエンティティ クラスは存在しません。

Instructor-Course_many-to-many_relationship

ただし、次のデータベース図で示すように、データベースには結合テーブルが必要です。

Instructor-Course_many-to-many_relationship_tables

Entity Framework によって CourseInstructor テーブルが自動的に作成されます。その読み取りと更新は、Instructor.CoursesCourse.Instructors ナビゲーション プロパティの読み取りと更新によって間接的に行います。

エンティティ関係図

次の図では、完成した School モデルに対して Entity Framework Power Tools で作成される図を示します。

School_data_model_diagram

多対多リレーションシップの線 (* 対 *) と一対多リレーションシップの線 (1 対 *) 以外にも、ここには InstructorOfficeAssignment エンティティの間の一対ゼロまたは一リレーションシップの線 (1 対 0..1)、および Instructor と Department エンティティの間のゼロまたは一対多リレーションシップの線 (0..1 対 *) があります。

データベース コンテキストにコードを追加する

次に、SchoolContext クラスに新しいエンティティを追加し、fluent API の呼び出しを使って一部のマッピングをカスタマイズします。 (API が "fluent" であるのは、次の例のように一連のメソッド呼び出しを 1 つのステートメントにまとめて使用することが多いためです)。

modelBuilder.Entity<Course>()
     .HasMany(c => c.Instructors).WithMany(i => i.Courses)
     .Map(t => t.MapLeftKey("CourseID")
         .MapRightKey("InstructorID")
         .ToTable("CourseInstructor"));

このチュートリアルでは、属性ではできないデータベースのマッピングについてのみ fluent API を使います。 ただし、fluent API を使用して、属性で実行できる書式設定、検証、およびマッピング規則のほとんどを指定することもできます。 MinimumLength などの一部の属性は fluent API で適用できません。 前に説明したように、MinimumLength ではスキーマは変更されず、クライアントとサーバーの側で検証規則が適用されるだけです

一部の開発者は fluent API のみを使用することを選ぶため、エンティティ クラスを "クリーン" な状態に保つことができます。必要に応じて、属性と fluent API を組み合わせて使用できます。fluent API のみを使用して実行できるカスタマイズがいくつかありますが、一般的は 2 つの方法のいずれかを選択して、できるだけ一貫性を保つためにそれを使用することをお勧めします。

新しいエンティティをデータ モデルに追加し、属性を使って行わなかったデータベース マッピングを実行するには、DAL\SchoolContext.cs のコードを次のコードに置き換えます。

using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace ContosoUniversity.DAL
{
   public class SchoolContext : DbContext
   {
      public DbSet<Course> Courses { get; set; }
      public DbSet<Department> Departments { get; set; }
      public DbSet<Enrollment> Enrollments { get; set; }
      public DbSet<Instructor> Instructors { get; set; }
      public DbSet<Student> Students { get; set; }
      public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
         modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

         modelBuilder.Entity<Course>()
             .HasMany(c => c.Instructors).WithMany(i => i.Courses)
             .Map(t => t.MapLeftKey("CourseID")
                 .MapRightKey("InstructorID")
                 .ToTable("CourseInstructor"));
      }
   }
}

OnModelCreating メソッドの新しいステートメントによって、多対多結合テーブルが構成されます。

  • InstructorCourse エンティティの間の多対多リレーションシップの場合は、コードで結合テーブルのテーブルと列の名前を指定します。 Code First を使うと、このコードを使わないで多対多リレーションシップを自動的に構成できますが、それを呼び出さない場合は、InstructorID 列に InstructorInstructorID などの既定の名前が使われます。

    modelBuilder.Entity<Course>()
        .HasMany(c => c.Instructors).WithMany(i => i.Courses)
        .Map(t => t.MapLeftKey("CourseID")
            .MapRightKey("InstructorID")
            .ToTable("CourseInstructor"));
    

次に示すコードは、属性の代わりに fluent API を使って、InstructorOfficeAssignment エンティティの間のリレーションシップを指定する方法の例です。

modelBuilder.Entity<Instructor>()
    .HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);

"fluent API" のステートメントがバックグラウンドで行っていることについて詳しくは、fluent API に関するブログ記事をご覧ください。

テスト データを使ってデータベースをシードする

作成した新しいエンティティのシード データを提供するには、Migrations\Configuration.cs ファイルのコードを次のコードに置き換えます。

namespace ContosoUniversity.Migrations
{
    using ContosoUniversity.Models;
    using ContosoUniversity.DAL;
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
    
    internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(SchoolContext context)
        {
            var students = new List<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") }
            };

            students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
            context.SaveChanges();

            var instructors = new List<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") }
            };
            instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
            context.SaveChanges();

            var departments = new List<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 }
            };
            departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
            context.SaveChanges();

            var courses = new List<Course>
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
            };
            courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
            context.SaveChanges();

            var officeAssignments = new List<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" },
            };
            officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s));
            context.SaveChanges();

            AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
            AddOrUpdateInstructor(context, "Chemistry", "Harui");
            AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
            AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");

            AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
            AddOrUpdateInstructor(context, "Trigonometry", "Harui");
            AddOrUpdateInstructor(context, "Composition", "Abercrombie");
            AddOrUpdateInstructor(context, "Literature", "Abercrombie");

            context.SaveChanges();

            var enrollments = new List<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();
        }

        void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
        {
            var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
            var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
            if (inst == null)
                crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
        }
    }
}

最初のチュートリアルで見たように、このコードのほとんどは新しいエンティティ オブジェクトを更新または作成し、テストで必要なサンプル データをプロパティに読み込んでいるだけです。 ただし、Instructor エンティティとの多対多リレーションシップを持つ Course エンティティがどのように処理されているかに注意してください。

var courses = new List<Course>
{
    new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
      DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
      Instructors = new List<Instructor>() 
    },
    ...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();

Course オブジェクトを作成するときは、コード Instructors = new List<Instructor>() を使って、Instructors ナビゲーション プロパティを空のコレクションとして初期化します。 これにより、Instructors.Add メソッドを使って、この Course に関連する Instructor エンティティを追加できるようになります。 空のリストを作成しなかった場合は、Instructors プロパティが null になり、Add メソッドがないため、これらのリレーションシップを追加できません。 また、コンストラクターにリストの初期化を追加することもできます。

移行を追加する

PMC から add-migration コマンドを入力します (update-database コマンドはまだ実行しないでください)。

add-Migration ComplexDataModel

この時点で update-database コマンドを実行しようとした場合は (まだ実行しないでください)、以下のようなエラーが表示されます。

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

場合によっては、既存のデータで移行を実行するときに、外部キー制約を満たすためにデータベースにスタブ データを挿入する必要があります。ここで行う必要があるのも、これです。 ComplexDataModel Up メソッドの生成されたコードでは、Course テーブルに null 非許容の DepartmentID 外部キーが追加されます。 コードの実行時に Course テーブルに既に行があるため、AddColumn 操作は失敗します。これは、SQL Server では、null にできない列にどの値を入れるべきか判断できないためです。 したがって、コードを変更して新しい列に既定値を設定し、"Temp" という名前のスタブ学科を作成して、既定の学科として機能するようにする必要があります。 その結果、既存の Course 行が、Up メソッドの実行後に "Temp" 学科に関連付けられます。 Seed メソッドで正しい学科に関連付けることができます。

<タイムスタンプ>_ComplexDataModel.cs ファイルを編集し、Course テーブルに DepartmentID 列を追加するコード行をコメントアウトし、以下の強調表示されたコードを追加します (コメント行も強調表示されています)。

CreateTable(
        "dbo.CourseInstructor",
        c => new
            {
                CourseID = c.Int(nullable: false),
                InstructorID = c.Int(nullable: false),
            })
        .PrimaryKey(t => new { t.CourseID, t.InstructorID })
        .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
        .ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
        .Index(t => t.CourseID)
        .Index(t => t.InstructorID);

    // Create  a department for course to point to.
    Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
    //  default value for FK points to department created above.
    AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1)); 
    //AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));

    AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));

Seed メソッドは実行すると、Department テーブルに行を挿入し、既存の Course 行をそれらの新しい Department 行に関連付けます。 UI にコースを追加していない場合は、"Temp" 学科または Course.DepartmentID 列の既定値は不要になります。 他のユーザーがアプリケーションを使ってコースを追加した可能性に対応するため、列から既定値を削除して "Temp" 学科を削除する前に、Seed メソッドのコードを更新して、(Seed メソッドの以前の実行によって挿入されたものだけでなく) すべての Course 行に有効な DepartmentID 値が含まれるようにする必要もあります。

データベースを更新する

<タイムスタンプ>_ComplexDataModel.cs ファイルの編集が完了したら、PMC で update-database コマンドを入力して移行を実行します。

update-database

Note

データを移行してスキーマを変更すると、他のエラーが発生する可能性があります。 解決できない移行エラーが発生した場合は、接続文字列のデータベース名を変更するか、データベースを削除できます。 最も簡単な方法は、Web.config ファイルでデータベースの名前を変更することです。 次の例は、名前が CU_Test に変更されていることを示しています。

<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;Integrated Security=SSPI;" 
      providerName="System.Data.SqlClient" />

新しいデータベースには移行するデータがないため、update-database コマンドがエラーなしで完了する可能性がはるかに高くなります。 データベースを削除する方法については、「Visual Studio 2012 からデータベースを削除する方法」をご覧ください。

失敗した場合は、PMC で次のコマンドを入力してデータベースを再初期化してみてください。

update-database -TargetMigration:0

前に行ったようにサーバー エクスプローラーでデータベースを開き、Tables ノードを展開して、テーブルがすべて作成されていることを確認します。 (前に開いたサーバー エクスプローラーをそのままにしている場合は、[更新] ボタンをクリックします)。

[サーバー エクスプローラー] ウィンドウを示すスクリーンショット。[学校コンテキスト] の下の [テーブル] フォルダーが開いています。

CourseInstructor テーブルのモデル クラスは作成しませんでした。 前に説明したように、これは InstructorCourse エンティティの間の多対多リレーションシップの結合テーブルです。

CourseInstructor テーブルを右クリックし、[テーブル データの表示] を選んで、Course.Instructors ナビゲーション プロパティに追加した Instructor エンティティの結果として、それにデータが含まれていることを確認します。

Table_data_in_CourseInstructor_table

コードを取得する

完成したプロジェクトのダウンロード

その他のリソース

他の Entity Framework リソースへのリンクは、「ASP.NET データ アクセス - 推奨リソース」にあります。

次のステップ

このチュートリアルでは、次の作業を行いました。

  • データ モデルをカスタマイズする
  • Student エンティティを更新する
  • Instructor エンティティを作成した
  • OfficeAssignment エンティティを作成した
  • Course エンティティを変更する
  • Department エンティティを作成する
  • Enrollment エンティティを変更する
  • データベース コンテキストにコードを追加する
  • テスト データを使ってデータベースをシードした
  • 移行を追加した
  • データベースを更新した

次の記事では、Entity Framework によってナビゲーション プロパティに読み込まれる関連データを読み取って表示する方法について説明します。