リレーションシップ、ナビゲーション プロパティ、および外部キー

この記事では、Entity Framework でエンティティ間のリレーションシップがどのように管理されるかの概要を説明します。 リレーションシップをマッピングしたり操作したりする方法についてのガイダンスも紹介します。

EF のリレーションシップ

リレーショナル データベースでは、テーブル間のリレーションシップ (関連付けとも呼ばれます) が外部キーによって定義されます。 外部キー (FK) は、2 つのテーブルのデータ間にリンクを確立および設定するために使用される単一の列または複数の列の組み合わせです。 一般に、一対一、一対多、多対多という 3 種類のリレーションシップが存在します。 一対多リレーションシップでは、リレーションシップの多側の End を表す外部キーがテーブルに定義されます。 多対多リレーションシップでは、両方の関連テーブルからの外部キーが主キーとなるような第 3 のテーブル (交差テーブルまたは結合テーブル) を定義することが必要となります。 一対一リレーションシップでは、主キーが外部キーとしての役割も兼ねており、どちらのテーブルにも別個の外部キー列はありません。

次の画像は、一対多リレーションシップに参加する 2 つのテーブルを示しています。 Course テーブルは依存テーブルです。そこに含まれている DepartmentID 列が Department テーブルにリンクしているためです。

Department and Course tables

Entity Framework では、関連付けまたはリレーションシップを通じて、エンティティを他のエンティティに関連付けることができます。 各リレーションシップには、そのリレーションシップの 2 つのエンティティのエンティティ型とその複数要素の接続性 (1、0 または 1、多) を表す 2 つの End が含まれています。 リレーションシップは、リレーションシップのどちらの End がプリンシパル ロールで、どちらが依存ロールであるかを記述した参照に関する制約によって制御できます。

ナビゲーション プロパティは、2 つのエンティティ型間の関連付けを導く手段となります。 各オブジェクトは、参加する各リレーションシップに対してナビゲーション プロパティを持つことができます。 ナビゲーション プロパティを使用すると、リレーションシップを両方向から見て管理し、複数要素の接続性が一、またはゼロか一の場合には参照オブジェクトを返し、複数要素の接続性が多の場合にはコレクションを返すことができます。 一方向のナビゲーションを定義することもできます。その場合、リレーションシップに参加する 2 つの型のうち、どちらか一方にだけナビゲーション プロパティを定義します。

モデルには、データベース内の外部キーにマッピングするプロパティを含めることをお勧めします。 外部キー プロパティを含めることにより、依存オブジェクトの外部キーの値を変更してリレーションシップを作成または変更することができます。 このような種類のアソシエーションは外部キー アソシエーションと呼ばれます。 接続解除エンティティの取り扱いでは、外部キーの使用がいっそう重要となります。 1 対 1 または 1 対 0..1 のリレーションシップを扱う際は、個別の外部キー列は存在しないことに注意してください。主キー プロパティが外部キーとしての役割を果たし、常にモデルに追加されます。

外部キー列がモデルに含まれていない場合、関連付け情報は独立したオブジェクトとして管理されます。 リレーションシップは、外部キーのプロパティではなくオブジェクト参照を通じて追跡されます。 この種の関連付けは "独立した関連付け" と呼ばれます。 "独立した関連付け" を変更するには、関連付けに参加する各エンティティ用に生成されるナビゲーション プロパティを変更するのが最も一般的な方法です。

モデルでは 1 つまたは両方のアソシエーションを使用するよう選択することができます。 ただし、外部キーだけを含む結合テーブルで連結された純粋な多対多リレーションシップがある場合、EF は独立した関連付けを使用して、そのような多対多リレーションシップを管理します。   

次の画像は、Entity Framework Designer を使用して作成された概念モデルを示したものです。 このモデルには、一対多リレーションシップに参加する 2 つのエンティティが存在します。 どちらのエンティティにもナビゲーション プロパティがあります。 Course は依存エンティティで、DepartmentID 外部キー プロパティが定義されています。

Department and Course tables with navigation properties

次のコード スニペットは、同じモデルを Code First で作成したものです。

public class Course
{
  public int CourseID { get; set; }
  public string Title { get; set; }
  public int Credits { get; set; }
  public int DepartmentID { get; set; }
  public virtual Department Department { get; set; }
}

public class Department
{
   public Department()
   {
     this.Courses = new HashSet<Course>();
   }  
   public int DepartmentID { get; set; }
   public string Name { get; set; }
   public decimal Budget { get; set; }
   public DateTime StartDate { get; set; }
   public int? Administrator {get ; set; }
   public virtual ICollection<Course> Courses { get; set; }
}

リレーションシップを構成またはマッピングする

以降このページでは、リレーションシップを使用してデータにアクセスし、それを操作する方法について取り上げます。 モデルにおけるリレーションシップの設定については、次のページを参照してください。

リレーションシップを作成、変更する

"外部キーの関連付け" では、リレーションシップを変更すると、EntityState.Unchanged 状態の依存オブジェクトが EntityState.Modified 状態に変わります。 独立リレーションシップでは、リレーションシップを変更しても依存オブジェクトの状態は更新されません。

外部キー プロパティとナビゲーション プロパティを使用して関連オブジェクトを関連付ける方法を次の例に示します。 外部キーの関連付けでは、どちらの方法でもリレーションシップを作成または変更できます。 独立アソシエーションでは、外部キー プロパティは使用できません。

  • 外部キー プロパティに新しい値を代入します。以下はその例です。

    course.DepartmentID = newCourse.DepartmentID;
    
  • 次のコードでは、外部キーを null に設定することでリレーションシップを削除しています。 外部キー プロパティは Null 許容でなければならないことに注意してください。

    course.DepartmentID = null;
    

    Note

    参照が追加済み状態である場合 (この例では、course オブジェクト)、SaveChanges が呼び出されるまで参照ナビゲーション プロパティは新しいオブジェクトのキー値と同期されません。 同期が行われない理由は、追加されたオブジェクトが保存されるまでオブジェクト コンテキストに永久キーが含まれていないからです。 リレーションシップを設定してすぐに新しいオブジェクトを完全に同期させる必要がある場合は、以下の方法のいずれかを使用してください。*

  • 新しいオブジェクトをナビゲーション プロパティに割り当てる。 次のコードでは、course と department との間にリレーションシップを作成します。 オブジェクトがコンテキストにアタッチされていると、coursedepartment.Courses コレクションに追加され、course オブジェクトの対応する外部キー プロパティは department のキー プロパティ値に設定されます。

    course.Department = department;
    
  • リレーションシップを削除するには、ナビゲーション プロパティを null に設定します。 .NET 4.0 に基づく Entity Framework を使用している場合は、関連 End が読み込まれてから null に設定する必要があります。 次に例を示します。

    context.Entry(course).Reference(c => c.Department).Load();
    course.Department = null;
    

    Entity Framework 5.0 以降は .NET 4.5 がベースとなるため、関連 End を読み込んでおかなくても、リレーションシップを null に設定できます。 次の方法を使用して現在の値を null に設定することもできます。

    context.Entry(course).Reference(c => c.Department).CurrentValue = null;
    
  • エンティティ コレクションのオブジェクトを削除または追加する。 たとえば、Course 型のオブジェクトを department.Courses コレクションに追加できます。 この操作は、特定の course と特定の department との間にリレーションシップを作成します。 オブジェクトがコンテキストにアタッチされている場合、course オブジェクトの外部キー プロパティと department 参照が適切な department に設定されます。

    department.Courses.Add(newCourse);
    
  • ChangeRelationshipState メソッドを使用して、2 つのエンティティ オブジェクト間の指定されたリレーションシップの状態を変更します。 このメソッドは、n 層アプリケーションと "独立した関連付け" を扱う際に最もよく使用されます (外部キーの関連付けには使用できません)。 また、このメソッドを使用するには、以下の例に示すように、ObjectContext まで辿る必要があります。
    次の例では、Instructors と Courses との間に多対多リレーションシップが存在します。 ChangeRelationshipState メソッドを呼び出して EntityState.Added パラメーターを渡すことで、2 つのオブジェクト間にリレーションシップが追加されたことを SchoolContext に認識させることができます。

    
    ((IObjectContextAdapter)context).ObjectContext.
      ObjectStateManager.
      ChangeRelationshipState(course, instructor, c => c.Instructor, EntityState.Added);
    

    リレーションシップを (追加するだけでなく) 更新する場合は、以前のリレーションシップを削除してから新たに追加する必要があります。

    ((IObjectContextAdapter)context).ObjectContext.
      ObjectStateManager.
      ChangeRelationshipState(course, oldInstructor, c => c.Instructor, EntityState.Deleted);
    

外部キーとナビゲーション プロパティとの間で変更を同期させる

前述したいずれかの方法で、コンテキストにアタッチされたオブジェクトのリレーションシップを変更した場合、Entity Framework は、外部キー、参照、コレクションを同期した状態に保つ必要があります。POCO エンティティに対しては、この同期がプロキシを使用して自動的に管理されます (リレーションシップ修正とも呼ばれます)。 詳細については、「プロキシの使用」を参照してください。

プロキシなしで POCO エンティティを使用している場合は、DetectChanges メソッドを呼び出して、コンテキストの関連オブジェクトを同期させる必要があります。 次の API では、DetectChanges 呼び出しが自動的にトリガーされることに注意してください。

  • DbSet.Add
  • DbSet.AddRange
  • DbSet.Remove
  • DbSet.RemoveRange
  • DbSet.Find
  • DbSet.Local
  • DbContext.SaveChanges
  • DbSet.Attach
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries
  • DbSet に対する LINQ クエリの実行

Entity Framework では、定義された関連付けから返されたエンティティの関連エンティティを読み込む際、一般にナビゲーション プロパティを使用します。 詳しくは、「関連オブジェクトの読み込み」をご覧ください。

Note

外部キー アソシエーションで依存オブジェクトの関連 End を読み込むと、現在メモリ内にある依存の外部キー値に基づいて関連オブジェクトが読み込まれます。

    // Get the course where currently DepartmentID = 2.
    Course course = context.Courses.First(c => c.DepartmentID == 2);

    // Use DepartmentID foreign key property
    // to change the association.
    course.DepartmentID = 3;

    // Load the related Department where DepartmentID = 3
    context.Entry(course).Reference(c => c.Department).Load();

独立アソシエーションの場合、依存オブジェクトの関連 End は現在データベース内にある外部キー値に基づいてクエリが行われます。 ただしリレーションシップが変更され、依存オブジェクトの参照プロパティがオブジェクト コンテキストに読み込まれた別のプリンシパル オブジェクトを指す場合、Entity Framework はクライアント上に定義されたとおりにリレーションシップを作成しようとします。

コンカレンシーを管理する

外部キーの関連付けも独立した関連付けも、エンティティ キーなど、モデルに定義されているエンティティのプロパティに基づいてコンカレンシー チェックが行われます。 EF Designer を使用してモデルを作成する場合、プロパティのコンカレンシー チェックが必要であることを指定するには、ConcurrencyMode 属性を fixed に設定します。 Code First を使用してモデルを定義している場合は、コンカレンシー チェックが必要なプロパティに ConcurrencyCheck 注釈を使用します。 Code First を使用している場合は、さらに TimeStamp 注釈を使用して、そのプロパティのコンカレンシー チェックが必要であることを指定する必要があります。 timestamp プロパティを定義できるのは、特定のクラスの中で 1 回だけです。 このプロパティは、Code First によって、データベースの null 非許容フィールドにマッピングされます。

コンカレンシー チェックとコンカレンシー解決に参加するエンティティを扱う場合は、常に外部キーの関連付けを使用することをお勧めします。

詳細については、コンカレンシーの競合の処理に関するページを参照してください。

重複するキーを使用する

重複するキーは、キーの一部のプロパティがエンティティの別のキーの一部でもある複合キーです。 独立アソシエーションで重複するキーを持つことはできません。 重複するキーを持つ外部キー アソシエーションを変更する場合は、オブジェクト参照を使用するよりも外部キー値を変更することをお勧めします。