リレーションシップ検出の規則

EF Core は、エンティティ型クラスに基づいてモデルを検出して構築するときに、一連の規則を使います。 このドキュメントでは、エンティティ型間のリレーションシップを検出し、構成するために使われる規則についてまとめています。

重要

マッピング属性またはモデル構築 API のいずれかを使ってリレーションシップを明示的に構成することで、ここで説明する規則をオーバーライドできます。

ヒント

次のコードは RelationshipConventions.cs にあります。

ナビゲーションの検出

リレーションシップの検出は、エンティティ型間のナビゲーションを検出することから始まります。

参照ナビゲーション

エンティティ型のプロパティは、次の場合に参照ナビゲーションとして検出されます。

  • プロパティはパブリックです。
  • プロパティにはゲッターとセッターがあります。
    • セッターはパブリックである必要はありません。プライベートまたは他のアクセシビリティを持つことができます。
    • セッターは init 専用にすることができます。
  • プロパティ型はエンティティ型です (またはその可能性があります)。 つまり、この型は:
    • 参照型である必要があります。
    • プリミティブ プロパティ型として明示的に構成することはできません。
    • 使われているデータベース プロバイダーによっては、プリミティブ プロパティ型としてマップすることはできません。
    • 使われているデータベース プロバイダーによってマップされたプリミティブ プロパティ型に自動的に変換することはできません
  • プロパティは静的ではありません。
  • プロパティはインデクサー プロパティではありません。

たとえば、次のようなエンティティ型を考えます。

public class Blog
{
    // Not discovered as reference navigations:
    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public Uri? Uri { get; set; }
    public ConsoleKeyInfo ConsoleKeyInfo { get; set; }
    public Author DefaultAuthor => new() { Name = $"Author of the blog {Title}" };

    // Discovered as a reference navigation:
    public Author? Author { get; private set; }
}

public class Author
{
    // Not discovered as reference navigations:
    public Guid Id { get; set; }
    public string Name { get; set; } = null!;
    public int BlogId { get; set; }

    // Discovered as a reference navigation:
    public Blog Blog { get; init; } = null!;
}

このような型の場合、Blog.AuthorAuthor.Blog は参照ナビゲーションとして検出されます。 一方、次のプロパティは参照ナビゲーションとして検出され "ません"。

  • Blog.Id (int はマップされたプリミティブ型であるため)
  • Blog.Title ('文字列' はマップされたプリミティブ型であるため)
  • Blog.Uri (Uri はマップされたプリミティブ型に自動的に変換されるため)
  • Blog.ConsoleKeyInfo (ConsoleKeyInfo は C# 値の型であるため)
  • Blog.DefaultAuthor (プロパティにセッターがないため)
  • Author.Id (Guid はマップされたプリミティブ型であるため)
  • Author.Name ('文字列' はマップされたプリミティブ型であるため)
  • Author.BlogId (int はマップされたプリミティブ型であるため)

コレクション ナビゲーション

エンティティ型のプロパティは、次の場合にコレクション ナビゲーションとして検出されます。

  • プロパティはパブリックです。
  • プロパティにゲッターがあります。 コレクション ナビゲーションにはセッターを含めることができますが、これは必須ではありません。
  • プロパティ型が IEnumerable<TEntity> であるか実装されており、TEntity はエンティティ型です (またはその可能性があります)。 つまり、TEntity の型は:
    • 参照型である必要があります。
    • プリミティブ プロパティ型として明示的に構成することはできません。
    • 使われているデータベース プロバイダーによっては、プリミティブ プロパティ型としてマップすることはできません。
    • 使われているデータベース プロバイダーによってマップされたプリミティブ プロパティ型に自動的に変換することはできません
  • プロパティは静的ではありません。
  • プロパティはインデクサー プロパティではありません。

たとえば、次のコードでは、Blog.TagsTag.Blogs の両方がコレクション ナビゲーションとして検出されています。

public class Blog
{
    public int Id { get; set; }
    public List<Tag> Tags { get; set; } = null!;
}

public class Tag
{
    public Guid Id { get; set; }
    public IEnumerable<Blog> Blogs { get; } = new List<Blog>();
}

ペアリング ナビゲーション

たとえば、エンティティ型 A からエンティティ型 B に向かうナビゲーションが検出されたら、次に、このナビゲーションに逆方向 (つまりエンティティ型 B からエンティティ型 A) に向かうものがあるかどうかを判断する必要があります。そのような逆方向が見つかった場合、2 つのナビゲーションがペアになって 1 つの双方向のリレーションシップが形成されます。

リレーションシップの型は、ナビゲーションとその逆方向が参照ナビゲーションかコレクション ナビゲーションかによって決まります。 具体的な内容は次のとおりです。

  • 一方のナビゲーションがコレクション ナビゲーションで、もう一方が参照ナビゲーションの場合、リレーションシップは一対多です。
  • 両方のナビゲーションが参照ナビゲーションの場合、リレーションシップは一対一です。
  • 両方のナビゲーションがコレクション ナビゲーションの場合、リレーションシップは多対多です。

このような各型のリレーションシップの検出を次の例に示します。

BlogPost の間には 1 つの一対多リレーションシップが検出され、Blog.PostsPost.Blog のナビゲーションがペアになって検出されます。

public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

BlogAuthor の間には 1 つの一対一リレーションシップが検出され、Blog.AuthorAuthor.Blog のナビゲーションがペアになって検出されます。

public class Blog
{
    public int Id { get; set; }
    public Author? Author { get; set; }
}

public class Author
{
    public int Id { get; set; }
    public int? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

PostTag の間には 1 つの多対多リレーションシップが検出され、Post.TagsTag.Posts のナビゲーションがペアになって検出されます。

public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; } = new List<Tag>();
}

public class Tag
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

注意

2 つのナビゲーションが 2 つの異なる一方向のリレーションシップを表す場合、ナビゲーションのこのペアリングは正しくない可能性があります。 この場合、2 つのリレーションシップを明示的に構成する必要があります。

リレーションシップのペアリングは、2 つの型間に 1 つのリレーションシップがある場合にのみ機能します。 2 つの型間に複数のリレーションシップがある場合は、明示的に構成する必要があります。

注意

この説明は、2 つの異なる型間のリレーションシップに関するものです。 ただし、同じ型がリレーションシップの両側に存在する可能性があるため、1 つの型に 2 つのナビゲーション両方が互いにペアリングされる場合があります。 これは自己参照リレーションシップと呼ばれます。

外部キー プロパティの検出

リレーションシップのナビゲーションが検出されるか、明示的に構成されると、リレーションシップの適切な外部キー プロパティを検出するためにこれらのナビゲーションが使われます。 次の場合、プロパティは外部キーとして検出されます。

  • プロパティ型が、プリンシパル エンティティ型の主キーまたは代替キーと互換性があります。
    • 型が同じである場合、または外部キー プロパティ型が主キーまたは代替キーのプロパティ型の null 許容バージョンである場合、型は互換性があります。
  • プロパティ名は、外部キー プロパティの名前付け規則のいずれかに一致します。 名前付け規則は次のとおりです。
    • <navigation property name><principal key property name>
    • <navigation property name>Id
    • <principal entity type name><principal key property name>
    • <principal entity type name>Id
  • さらに、モデル構築 API を使って依存側を明示的に構成し、依存の主キーに互換性がある場合、依存の主キーも外部キーとして使われます。

ヒント

"Id" のサフィックスは任意の大文字と小文字を使用できます。

次のエンティティ型は、これらの各名前付け規則の例を示します。

Post.TheBlogKey はパターン <navigation property name><principal key property name> と一致するため、外部キーとして検出されます。

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? TheBlogKey { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.TheBlogID はパターン <navigation property name>Id と一致するため、外部キーとして検出されます。

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? TheBlogID { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.BlogKey はパターン <principal entity type name><principal key property name> と一致するため、外部キーとして検出されます。

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? BlogKey { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.Blogid はパターン <principal entity type name>Id と一致するため、外部キーとして検出されます。

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? Blogid { get; set; }
    public Blog? TheBlog { get; set; }
}

注意

一対多ナビゲーションの場合、外部キーのプロパティは、依存エンティティになるため、参照ナビゲーションを持つ型に設定する必要があります。 一対一リレーションシップの場合、どの型がリレーションシップの依存側を表すかを決定するために、外部キー プロパティの検出が使われます。 外部キー プロパティが検出されない場合、依存側は HasForeignKey を使って構成する必要があります。 この例については、「一対一リレーションシップ」を参照してください。

上記の規則は複合外部キーにも適用されます。前述のように、複合の各プロパティは、主キーまたは代替キーの対応するプロパティと互換性のある型を持つ必要があり、各プロパティ名は上記の名前付け規則のいずれかに一致する必要があります。

カーディナリティの決定

EF は、検出されたナビゲーションと外部キーのプロパティを使って、リレーションシップのカーディナリティとそのプリンシパルと依存側を決定します。

  • ペアになっていない参照ナビゲーションが 1 つある場合、リレーションシップは一方向の一対多として構成され、参照ナビゲーションは依存側になります。
  • ペアになっていないコレクション ナビゲーションが 1 つある場合、リレーションシップは一方向の一対多として構成され、コレクション ナビゲーションはプリンシパル側になります。
  • ペアになっている参照およびコレクション ナビゲーションが複数ある場合、リレーションシップは双方向の一対多として構成され、コレクション ナビゲーションはプリンシパル側になります。
  • 参照ナビゲーションが別の参照ナビゲーションとペアになっている場合:
    • 一方に外部キー プロパティが検出され、もう一方に検出されなかった場合、リレーションシップは双方向の一対一として構成され、外部キー プロパティは依存側になります。
    • それ以外の場合は依存側を決定できず、依存側を明示的に構成する必要があることを示す例外が EF からスローされます。
  • コレクション ナビゲーションが別のコレクション ナビゲーションとペアになっている場合、リレーションシップは双方向の多対多として構成されます。

シャドウ外部キー プロパティ

EF がリレーションシップの依存側を決定しても、外部キー プロパティが検出されなかった場合、EF は外部キーを表すシャドウ プロパティを作成します。 シャドウ プロパティ:

  • リレーションシップのプリンシパル側の主キーまたは代替キーのプロパティ型を持ちます。
    • この型は既定で null 許容になり、リレーションシップは既定でオプションになります。
  • 依存側にナビゲーションがある場合、シャドウ外部キー プロパティは、このナビゲーション名とプライマリまたは代替キー プロパティ名を連結した名前を使います。
  • 依存側にナビゲーションがない場合、シャドウ外部キー プロパティは、プリンシパル エンティティ型名とプライマリまたは代替キー プロパティ名を連結した名前を使います。

連鎖削除

規則により、必須リレーションシップは連鎖削除に構成されています。 オプションのリレーションシップは、連鎖削除されないように構成されています。

多対多

多対多リレーションシップはプリンシパル側と依存側を持たず、どちら側にも外部キー プロパティはありません。 代わりに、多対多リレーションシップは、多対多のいずれかの側を指す外部キーのペアを含む結合エンティティ型を使います。 規則によって多対多リレーションシップが検出される、次のエンティティ型を考えてみましょう。

public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; } = new List<Tag>();
}

public class Tag
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

この検出で使われる規則は次のとおりです。

  • 結合エンティティ型は <left entity type name><right entity type name> という名前です。 そのため、この例では PostTag です。
    • 結合テーブルには、結合エンティティ型と同じ名前です。
  • 結合エンティティ型には、リレーションシップの各方向に対して外部キー プロパティが与えられます。 これらには <navigation name><principal key name> という名前が付けられます。 そのため、この例では、外部キー プロパティは PostsIdTagsId です。
    • 一方向の多対多の場合、ナビゲーションが関連付けられていない外部キー プロパティは <principal entity type name><principal key name> という名前が付けられます。
  • 外部キー プロパティは null 非許容であるため、結合エンティティに対する両方のリレーションシップが必要です。
    • 連鎖削除規則は、これらのリレーションシップが連鎖削除に構成されることを意味します。
  • 2 つの外部キー プロパティから構成される複合主キーを使って、結合エンティティ型が構成されます。 そのため、この例で主キーは PostsIdTagsId から構成されます。

この結果、次の EF モデルになります。

Model:
  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Skip navigations:
      Tags (ICollection<Tag>) CollectionTag Inverse: Posts
    Keys:
      Id PK
  EntityType: Tag
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Skip navigations:
      Posts (ICollection<Post>) CollectionPost Inverse: Tags
    Keys:
      Id PK
  EntityType: PostTag (Dictionary<string, object>) CLR Type: Dictionary<string, object>
    Properties:
      PostsId (no field, int) Indexer Required PK FK AfterSave:Throw
      TagsId (no field, int) Indexer Required PK FK Index AfterSave:Throw
    Keys:
      PostsId, TagsId PK
    Foreign keys:
      PostTag (Dictionary<string, object>) {'PostsId'} -> Post {'Id'} Cascade
      PostTag (Dictionary<string, object>) {'TagsId'} -> Tag {'Id'} Cascade
    Indexes:
      TagsId

また、SQLite を使う場合は、次のデータベース スキーマに変換されます。

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tag" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tag_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tag" ("Id") ON DELETE CASCADE);

CREATE INDEX "IX_PostTag_TagsId" ON "PostTag" ("TagsId");

インデックス

規則により、EF は外部キーの 1 つ以上のプロパティに対してデータベース インデックスを作成します。 作成されるインデックスの型は、以下によって決まります。

  • リレーションシップのカーディナリティ
  • リレーションシップがオプションか必須か
  • 外部キーを構成するプロパティの数

一対多リレーションシップの場合、規則により、単純なインデックスが作成されます。 オプションと必須のリレーションシップに対して同じインデックスが作成されます。 たとえば、SQLite の場合:

CREATE INDEX "IX_Post_BlogId" ON "Post" ("BlogId");

または SQL Server の場合:

CREATE INDEX [IX_Post_BlogId] ON [Post] ([BlogId]);

必須の一対一リレーションシップの場合、一意のインデックスが作成されます。 たとえば、SQLite の場合:

CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId");

または SQL Server の場合:

CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]);

オプションの一対一リレーションシップの場合、SQLite に作成されるインデックスは同じです。

CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId");

ただし、SQL Server では、null 外部キー値をより適切に処理するために IS NOT NULL フィルターが追加されます。 次に例を示します。

CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]) WHERE [BlogId] IS NOT NULL;

複合外部キーの場合、すべての外部キー列をカバーするインデックスが作成されます。 次に例を示します。

CREATE INDEX "IX_Post_ContainingBlogId1_ContainingBlogId2" ON "Post" ("ContainingBlogId1", "ContainingBlogId2");

注意

既存のインデックスまたは主キーの制約によって既にカバーされているプロパティに対しては、EF はインデックスを作成しません。

EF が外部キーのインデックスを作成しないようにする方法

インデックスにはオーバーヘッドがあり、こちらで質問されているように、すべての FK 列にインデックスを作成するのが常に適切であるとは限りません。 これを実現するには、モデルを構築するときに ForeignKeyIndexConvention を削除できます。

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

必要に応じて、このような必要な外部キー列にインデックスを明示的に作成できます

外部キー制約の名前

規則により、外部キー制約には FK_<dependent type name>_<principal type name>_<foreign key property name> という名前が付けられます。 複合外部キーの場合、<foreign key property name> は、アンダースコアで区切られた外部キー プロパティ名の一覧になります。

その他の技術情報