ASP.NET MVC アプリケーションのより複雑なデータ モデルの作成 (4/10)

作成者: Tom Dykstra

Contoso University サンプル Web アプリケーションでは、Entity Framework 5 Code First と Visual Studio 2012 を使用して ASP.NET MVC 4 アプリケーションを作成する方法を示します。 チュートリアル シリーズについては、シリーズの最初のチュートリアルをご覧ください。

Note

解決できない問題が発生した場合は、 完了した章をダウンロード して、問題を再現してみてください。 通常、コードを完成したコードと比較することで、問題の解決策を見つけることができます。 一般的なエラーとその解決方法については、「エラーと回避策」を参照してください。

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

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

School_class_diagram

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

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

DataType 属性

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

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

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { 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 属性を使用して、アプリケーションで型固有の機能を自動的に提供することもできます。 たとえば、mailto:DataType.EmailAddress 用にリンクを作成したり、HTML5 をサポートするブラウザーで DataType.Date に日付セレクターを指定したりできます。 DataType 属性は、HTML 5 ブラウザーが理解できる HTML 5 data- (発音されたデータ ダッシュ) 属性を出力します。 DataType 属性は検証を提供しません。

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

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

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }

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

DisplayFormat 属性は単独で使用できますが、通常は DataType 属性も使用することをお勧めします。 属性は DataType 、画面に表示する方法とは対照的にデータの セマンティクス を伝達し、 では得 DisplayFormatられない次の利点を提供します。

  • ブラウザーで HTML5 機能を有効にすることができます (カレンダー コントロール、ロケールに適した通貨記号、電子メール リンクなどを表示する場合など)。
  • 既定では、ブラウザーは ロケールに基づいて正しい形式を使用してデータをレンダリングします。
  • DataType 属性を使用すると、MVC で適切なフィールド テンプレートを選択してデータをレンダリングできます (単独で使用する場合は DisplayFormat が文字列テンプレートを使用します)。 詳細については、「Brad Wilson の ASP.NET MVC 2 テンプレート」を参照してください。 (MVC 2 用に記述されていますが、この記事は現在のバージョンの ASP.NET MVC にも適用されます)。

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

[Student Index]\(学生インデックス\) ページをもう一度実行し、登録日の時刻が表示されていないことに注意してください。 モデルを使用 Student するビューについても同じことが当てはまります。

Students_index_page_with_formatted_date

The StringLengthAttribute

属性を使用して、データ検証ルールとメッセージを指定することもできます。 たとえば、ユーザーが 50 文字を超える名前を入力しないようにする必要があるとします。 この制限を追加するには、次の例に示すように、 プロパティと FirstMidName プロパティに LastNameStringLength 属性を追加します。

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { 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 属性と同様の機能を提供しますが、クライアント側の検証は提供しません。

アプリケーションを実行し、[ 学生 ] タブをクリックします。次のエラーが発生します。

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

データベース モデルは、データベース スキーマを変更する必要がある方法で変更され、Entity Framework によって検出されました。 UI を使用してデータベースに追加したデータを失うことなく、移行を使用してスキーマを更新します。 メソッドによって作成されたデータを Seed 変更した場合、 メソッドで使用している AddOrUpdate メソッドが原因で元の状態に Seed 戻されます。 (AddOrUpdate は、データベース用語からの "upsert" 操作と同じです)。

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

add-migration MaxLengthOnNames
update-database

コマンドはadd-migration MaxLengthOnNamestimeStamp>_MaxLengthOnNames.cs という名前<のファイルを作成します。 このファイルには、現在のデータ モデルに一致するようにデータベースを更新するコードが含まれています。 移行ファイル名の前に付加されたタイムスタンプは、移行を注文するために Entity Framework によって使用されます。 複数の移行を作成した後、データベースを削除した場合、または Migrations を使用してプロジェクトをデプロイすると、すべての移行が作成された順序で適用されます。

[作成] ページを実行し、50 文字を超える名前を入力します。 50 文字を超えるとすぐに、クライアント側の検証でエラー メッセージがすぐに表示されます。

クライアント側の val エラー

列属性

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

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

次の強調表示されたコードに示すように、 System.ComponentModel.DataAnnotations.Schema の using ステートメントと列名属性 FirstMidName を プロパティに追加します。

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { 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

[サーバー エクスプローラー (Express for Web を使用している場合はデータベース エクスプローラー)] で、Student テーブルをダブルクリックします。

Server エクスプローラー の Student テーブルを示すスクリーンショット。

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

Server エクスプローラー の Student テーブルを示すスクリーンショット。前のスクリーンショットの [名] 行が [First Mid Name]\(最初の中間名\) と読み上げられます。

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

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 InstructorID { 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; }

        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

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

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

必須属性と表示属性

プロパティのLastName属性は、必須フィールドであり、テキスト ボックスのキャプションが "Last Name" (スペースのない "LastName" であるプロパティ名ではなく) であることを指定し、値を 50 文字以下にすることはできません。

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

StringLength 属性は、データベースの最大長を設定し、クライアント側とサーバー側の検証を ASP.NET MVC に提供します。 この属性で最小長を指定することもできますが、最小値はデータベース スキーマに影響しません。 DateTime、int、double、float などの値型には、 Required 属性 は必要ありません。 値型には null 値を割り当てることができないので、本質的に必要です。 Required 属性を削除し、属性の最小長パラメーターにStringLength置き換えることができます。

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

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

public class Instructor
{
   public int InstructorID { 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; }

   public string FullName
   {
      get { return LastName + ", " + FirstMidName; }
   }

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

FullName 計算プロパティ

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

public string FullName
{
    get { return LastName + ", " + FirstMidName; }
}

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

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

インストラクターは任意の数のコースを教えることができるので Courses 、 はエンティティの Course コレクションとして定義されます。 ビジネス ルールでは、講師は最大 1 つのオフィスしか持てないと規定されているため OfficeAssignment 、1 つの OfficeAssignment エンティティとして定義されます (オフィスが割り当てられていない場合もあります null )。

public virtual ICollection<Course> Courses { get; set; }
public virtual 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]
        [ForeignKey("Instructor")]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

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

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

キー属性

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

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

エンティティに独自のKey主キーがあるが、プロパティに または IDとは異なるclassnameID名前を付ける場合は、 属性を使用することもできます。 既定では、EF はキーをデータベース以外で生成されたものとして扱います。これは、列が識別リレーションシップ用であるためです。

ForeignKey 属性

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

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

チュートリアルの後半では、fluent API を使用してこのリレーションシップを構成する方法について説明します。

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

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

Instructor ナビゲーション プロパティに属性を [Required] 配置して、関連する講師が存在する必要があることを指定できますが、InstructorID 外部キー (このテーブルのキーでもあります) は 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; }

      [Display(Name = "Department")]
      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; }
   }
}

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

DatabaseGenerated 属性

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

[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 エンティティの作成

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)]
      public DateTime StartDate { get; set; }

      [Display(Name = "Administrator")]
      public int? InstructorID { get; set; }

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

列属性

以前は 、Column 属性 を使用して列名マッピングを変更しました。 エンティティのコードDepartmentでは、 属性をColumn使用して SQL データ型マッピングを変更し、データベースの SQL Server money 型を使用して列を定義します。

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

Entity Framework は通常、プロパティに対して定義した CLR 型に基づいて適切なSQL Serverデータ型を選択するため、列マッピングは一般に必須ではありません。 CLR decimal 型は SQL Server の decimal 型にマップされます。 ただし、この場合、列が通貨額を保持することがわかっており、その場合は money データ型の方が適しています。

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

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

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

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

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

    Note

    規則により、Entity Framework では null 非許容の外部キーと多対多リレーションシップに対して連鎖削除が有効になります。 これにより、循環連鎖削除ルールが発生する可能性があります。これにより、初期化子コードの実行時に例外が発生します。 たとえば、プロパティを Department.InstructorID null 許容として定義しなかった場合、初期化子の実行時に次の例外メッセージが表示されます。"参照関係により、循環参照が許可されません"。ビジネス ルールでプロパティが null 非許容として必要な場合は、次の fluent API を使用してリレーションシップの連鎖削除を無効にする必要 InstructorID があります。

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

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 StudentID { get; set; }

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

      [StringLength(50, MinimumLength = 1, 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)]
      [Display(Name = "Enrollment Date")]
      public DateTime EnrollmentDate { get; set; }

      public string FullName
      {
         get { return LastName + ", " + FirstMidName; }
      }

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

登録エンティティ

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 のみです。 その場合、データベースにペイロード (または純粋結合テーブル) がない 多対多 結合テーブルに対応するため、モデル クラスを作成する必要はまったくありません。 Instructorエンティティと Course エンティティには、このような多対多リレーションシップがあり、ご覧のように、それらの間にエンティティ クラスはありません。

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 対多のリレーションシップ行 (1 から *) に加えて、and エンティティ間の 1 対 0 または 1 のリレーションシップ線 (1 から 0..1)、および OfficeAssignment Instructor エンティティと Department エンティティ間Instructorのゼロまたは一対多リレーションシップライン (0..1 から *) を確認できます。

データベース コンテキストにコードを追加してデータ モデルをカスタマイズする

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

このチュートリアルでは、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 メソッドの新しいステートメントは、多対多結合テーブルを構成します。

  • エンティティと Course エンティティの間Instructorの多対多リレーションシップの場合、結合テーブルのテーブル名と列名を指定します。 Code First では、このコードを使用せずに多対多リレーションシップを構成できますが、呼び出さない場合は、列の などの既定の名前 InstructorInstructorIDInstructorID 取得されます。

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

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

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

"fluent API" ステートメントがバックグラウンドで行っていることの詳細については、 Fluent API のブログ投稿を参照してください。

テスト データでデータベースをシードする

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

namespace ContosoUniversity.Migrations
{
   using System;
   using System.Collections.Generic;
   using System.Data.Entity;
   using System.Data.Entity.Migrations;
   using System.Linq;
   using ContosoUniversity.Models;
   using ContosoUniversity.DAL;

   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").InstructorID },
                new Department { Name = "Mathematics", Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").InstructorID },
                new Department { Name = "Engineering", Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").InstructorID },
                new Department { Name = "Economics",   Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").InstructorID }
            };
         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").InstructorID, 
                    Location = "Smith 17" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Harui").InstructorID, 
                    Location = "Gowan 27" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").InstructorID, 
                    Location = "Thompson 304" },
            };
         officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.Location, 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").StudentID, 
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, 
                    Grade = Grade.A 
                },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, 
                    Grade = Grade.C 
                 },                            
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, 
                    Grade = Grade.B
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").StudentID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").StudentID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B         
                 },
                new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Barzdukas").StudentID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Li").StudentID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Justice").StudentID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B         
                 }
            };

         foreach (Enrollment e in enrollments)
         {
            var enrollmentInDataBase = context.Enrollments.Where(
                s =>
                     s.Student.StudentID == 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));
      }
   }
}

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

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

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

移行の追加とデータベースの更新

PMC から、次のコマンドを add-migration 入力します。

PM> add-Migration Chap4

この時点でデータベースを更新しようとすると、次のエラーが発生します。

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

<timestamp>_Chap4.cs ファイルを編集し、次のコードを変更します (SQL ステートメントを追加し、ステートメントを変更しますAddColumn)。

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));
    AddForeignKey("dbo.Course", "DepartmentID", "dbo.Department", "DepartmentID", cascadeDelete: true);
    CreateIndex("dbo.Course", "DepartmentID");
}

public override void Down()
{

(新しい行を追加するときに、既存 AddColumn の行をコメントアウトまたは削除してください。または、コマンドを入力するとエラーが update-database 発生します)。

既存のデータを使用して移行を実行する場合、外部キー制約を満たすためにスタブ データをデータベースに挿入する必要があり、それが現在行っていることです。 生成されたコードは、null 非許容 DepartmentID 外部キーをテーブルに Course 追加します。 コードの実行時にCourseテーブルに行が既に存在する場合、AddColumnSQL Serverは null にできない列に格納する値がわからないため、操作は失敗します。 そのため、新しい列に既定値を指定するようにコードを変更し、"Temp" という名前のスタブ部門を作成して既定の部門として機能しました。 その結果、このコードの実行時に既存 Course の行がある場合、それらはすべて "Temp" 部門に関連付けられます。

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

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

Note

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

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

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

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

サーバー エクスプローラー データベースを示すスクリーンショット。[テーブル] ノードが展開されます。

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

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

Table_data_in_CourseInstructor_table

まとめ

これで、より複雑なデータ モデルと対応するデータベースの準備ができました。 次のチュートリアルでは、関連するデータにアクセスするさまざまな方法について学習します。

他の Entity Framework リソースへのリンクは、 ASP.NET データ アクセス コンテンツ マップにあります。