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

多対多リレーションシップは、あるエンティティ型の任意の数のエンティティが、同じまたは別のエンティティ型の任意の数のエンティティに関連付けられる場合に使われます。 たとえば、Post に多くの Tags を関連付け、さらにそれぞれの Tag に任意の数の Posts を関連付けることができます。

多対多リレーションシップについて

多対多リレーションシップは、一対多リレーションシップや一対一リレーションシップとは異なり、外部キーのみを使ってシンプルに表現することができません。 代わりに、リレーションシップの両側を "結合" するために、追加のエンティティ型が必要になります。 これは "結合エンティティ型" と呼ばれ、リレーショナル データベースの "結合テーブル" にマップされます。 この結合エンティティ型のエンティティには外部キー値のペアが含まれます。各ペアの 1 つはリレーションシップの一方のエンティティを指し、もう 1 つはリレーションシップのもう一方の側のエンティティを指します。 そのため、それぞれの結合エンティティ (および結合テーブル内の各行) は、リレーションシップ内のエンティティ型間の 1 つの関連付けを表します。

EF Core では、結合エンティティ型を隠し、バックグラウンドで管理することができます。 これにより、多対多リレーションシップのナビゲーションを自然な方法で使い、必要に応じて各側にエンティティを追加したり削除したりすることができます。 ただし、その全体的な動作と、特にリレーショナル データベースへのマッピングが適切になるように、バックグラウンドで何が起こっているのかを理解すると役立ちます。 まずは、投稿とタグの間の多対多リレーションシップを表すリレーショナル データベースのスキーマの設定を見てみましょう。

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

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" 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_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

このスキーマでは、PostTag が結合テーブルです。 これには 2 つの列が含まれています。すなわち、Posts テーブルの主キーに対する外部キーである PostsId と、Tags テーブルの主キーに対する外部キーである TagsId です。 そのため、このテーブルの各行は、1 つの Post と 1 つの Tag の間の関連付けを表します。

EF Core では、このスキーマの単純化したマッピングは 3 つのエンティティ型 (テーブルごとに 1 つ) で構成されます。 これらのエンティティ型がそれぞれ 1 つの .NET クラスで表される場合、それらのクラスは次のようになります。

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostsId { get; set; }
    public int TagsId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

このマッピングには、多対多リレーションシップではなく、2 つの一対多リレーションシップがあることがわかります (結合テーブルで定義されている外部キーごとに 1 つ)。 これは、これらのテーブルをマップする不当な方法というわけではありませんが、2 つの一対多リレーションシップではなく 1 つの多対多リレーションシップを表すという結合テーブルの意図は反映されていません。

EF では、2 つのコレクション ナビゲーション (1 つは関連する Tags を含む Post に対して、逆は関連する Posts を含む Tag に対して) を導入することで、より自然なマッピングを実現できます。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int PostsId { get; set; }
    public int TagsId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

ヒント

これらの新しいナビゲーションは "スキップ ナビゲーション" と呼ばれます。結合エンティティをスキップして、多対多リレーションシップのもう一方の側に直接アクセスできるようにするためです。

次の例に示すように、多対多リレーションシップはこのような方法でマップできます。つまり、結合エンティティの .NET クラスと、2 つの一対多リレーションシップの両方のナビゲーション "および" スキップ ナビゲーションを、エンティティ型で公開します。 ただし、EF では結合エンティティを透過的に管理できます。そのために .NET クラスを定義することはなく、2 つの一対多リレーションシップのナビゲーションも使用しません。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

実際、EF のモデル構築の規則では、既定で、ここで示す Post 型と Tag 型は、このセクションの上部にあるデータベース スキーマの 3 つのテーブルにマップされます。 結合の種類を明示的に使わないこのマッピングは、通常、"多対多" という用語が意味するものに相当します。

以下のセクションでは、各マッピングを実現するために必要な構成など、多対多リレーションシップの例を示します。

ヒント

以下のすべての例のコードは、ManyToMany.cs にあります。

基本的な多対多

多対多の最も基本的なケースでは、リレーションシップの両端のエンティティ型が、両方ともコレクション ナビゲーションを持ちます。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

このリレーションシップは規則によってマップされます。 必要ではありませんが、このリレーションシップの同等の明示的な構成を、学習用に次に示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts);
}

この明示的な構成でも、リレーションシップの多くの側面はやはり規則によって構成されます。 より完全な明示的な構成を、学習目的でもう一度次に示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            "PostTag",
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagsId").HasPrincipalKey(nameof(Tag.Id)),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostsId").HasPrincipalKey(nameof(Post.Id)),
            j => j.HasKey("PostsId", "TagsId"));
}

重要

必要ない場合でも、すべてを完全に構成しようとしないでください。 上記のように、コードはすぐに複雑になり、間違いが発生しやすくなります。 さらに、上記の例でも、モデルには規則によって構成されるものが多くあります。 EF モデル内のすべてを常に明示的に完全に構成できると考えるのは、現実的ではありません。

リレーションシップが規則によって構築されるかどうか、または示されている明示的な構成のいずれかを使うかどうかに関係なく、生成されるマップされたスキーマ (SQLite を使用) は次のようになります。

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

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" 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_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

ヒント

Database First フローを使って既存のデータベースから DbContext をスキャフォールディングする場合、EF Core 6 以降では、データベース スキーマでこのパターンを検索し、このドキュメントで説明しているように多対多リレーションシップをスキャフォールディングします。 この動作は、カスタム T4 テンプレートを使って変更できます。 その他のオプションについては、「"マップされた結合エンティティがない多対多リレーションシップは、スキャフォールディングされるようになりました"」を参照してください。

重要

現在、EF Core では Dictionary<string, object> を使って、.NET クラスが構成されていない結合エンティティのインスタンスを表します。 ただし、パフォーマンスを向上させるために、今後の EF Core リリースでは別の型が使われる可能性があります。 明示的に構成されている場合を除いて、結合の種類が Dictionary<string, object> であることに依存しないでください。

名前付き結合テーブルを使った多対多

前の例では、結合テーブルには規則によって PostTag という名前が付けられました。 UsingEntity を使って明示的な名前を指定できます。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity("PostsToTagsJoinTable");
}

マッピングに関するその他すべては同じままで、結合テーブルの名前のみが変更されます。

CREATE TABLE "PostsToTagsJoinTable" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostsToTagsJoinTable" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostsToTagsJoinTable_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostsToTagsJoinTable_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

結合テーブルの外部キー名を使った多対多

前の例に続いて、結合テーブル内の外部キー列の名前も変更できます。 これには、2 つの方法があります。 1 つ目は、結合エンティティで外部キー プロパティの名前を明示的に指定することです。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagForeignKey"),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostForeignKey"));
}

2 つ目の方法は、プロパティは規則に従った名前のままにして、これらのプロパティを異なる列名にマップすることです。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            j =>
            {
                j.Property("PostsId").HasColumnName("PostForeignKey");
                j.Property("TagsId").HasColumnName("TagForeignKey");
            });
}

どちらの場合も、マッピングは同じままで、外部キー列の名前のみが変更されます。

CREATE TABLE "PostTag" (
    "PostForeignKey" INTEGER NOT NULL,
    "TagForeignKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
    CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

ヒント

ここでは示されていませんが、前の 2 つの例を組み合わせて、結合テーブル名とその外部キー列の名前をマップ変更できます。

結合エンティティのクラスを使った多対多

ここまでの例では、結合テーブルは共有型のエンティティ型に自動的にマップされていました。 これにより、エンティティ型に対して専用クラスを作成する必要がなくなります。 ただし、そのようなクラスを用意して、簡単に参照できるようにすると便利な場合があります。以下の後の例に示すように、ナビゲーションまたはペイロードがクラスに追加される場合は特にそうです。 これを行うには、まず、PostTag の既存の型に加えて、結合エンティティの型 PostTag を作成します。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
}

ヒント

クラスには任意の名前を指定できますが、リレーションシップの両端にある型の名前を組み合わせるのが一般的です。

これで、UsingEntity メソッドを使って、これをリレーションシップの結合エンティティ型として構成できます。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

PostIdTagId は外部キーとして自動的に取得され、結合エンティティ型の複合主キーとして構成されます。 外部キーに使うプロパティは、それらが EF 規則に一致しない場合のために明示的に構成できます。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>().WithMany().HasForeignKey(e => e.TagId),
            r => r.HasOne<Post>().WithMany().HasForeignKey(e => e.PostId));
}

この例の結合テーブルのマップされたデータベース スキーマは、構造的には前の例と同じですが、一部の列名が異なります。

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

結合エンティティへのナビゲーションを使った多対多

前の例に続いて、結合エンティティを表すクラスを作成したので、このクラスを参照するナビゲーションを簡単に追加できるようになりました。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
}

重要

この例に示すように、結合エンティティ型へのナビゲーションを、多対多リレーションシップの両端間のスキップ ナビゲーション "に加えて" 使用できます。 つまり、スキップ ナビゲーションを使って自然な方法で多対多リレーションシップと対話できる一方、結合エンティティ自体をより細かく制御する必要がある場合は、結合エンティティ型へのナビゲーションを使うことができます。 ある意味で、このマッピングは、シンプルな多対多マッピングと、データベース スキーマとより明示的に一致するマッピングとの間で、両方の世界のベストを提供するものです。

UsingEntity 呼び出しでは何も変更する必要はありません。結合エンティティへのナビゲーションは規則によって取得されるためです。 したがって、この例の構成は、最後の例の構成と同じです。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

ナビゲーションは、規則によって決定できない場合に対して明示的に構成できます。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>().WithMany(e => e.PostTags),
            r => r.HasOne<Post>().WithMany(e => e.PostTags));
}

モデルにナビゲーションを含めても、マップされたデータベース スキーマは影響を受けません。

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

結合エンティティとの間のナビゲーションを使った多対多

前の例では、多対多リレーションシップの両端にあるエンティティ型から結合エンティティ型へのナビゲーションを追加しました。 ナビゲーションは、もう一方の方向、または両方向に追加することもできます。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

UsingEntity 呼び出しでは何も変更する必要はありません。結合エンティティへのナビゲーションは規則によって取得されるためです。 したがって、この例の構成は、最後の例の構成と同じです。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

ナビゲーションは、規則によって決定できない場合に対して明示的に構成できます。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags));
}

モデルにナビゲーションを含めても、マップされたデータベース スキーマは影響を受けません。

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

ナビゲーションと変更した外部キーを使った多対多

前の例では、結合エンティティ型との間のナビゲーションを使った多対多を示しました。 この例も同じですが、ここでは使う外部キー プロパティも変更します。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostForeignKey { get; set; }
    public int TagForeignKey { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

ここでも、UsingEntity メソッドを使ってこれを構成します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasForeignKey(e => e.TagForeignKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasForeignKey(e => e.PostForeignKey));
}

マップされたデータベース スキーマは次のようになります。

CREATE TABLE "PostTag" (
    "PostForeignKey" INTEGER NOT NULL,
    "TagForeignKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
    CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

一方向の多対多

注意

一方向の多対多リレーションシップは、EF Core 7 で導入されました。 以前のリリースでは、回避策としてプライベート ナビゲーションを使用できました。

多対多リレーションシップの両側にナビゲーションを含める必要はありません。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
}

EF では、これが一対多ではなく多対多のリレーションシップである必要があることを知るために、何らかの構成が必要です。 これは HasManyWithMany を使って行われますが、ナビゲーションがない側には引数が渡されません。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany();
}

ナビゲーションを削除しても、データベース スキーマには影響しません。

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

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

多対多とペイロードを含む結合テーブル

これまでの例では、結合テーブルは、各関連付けを表す外部キーのペアを格納するためにのみ使われていました。 ただし、これを使って、関連付けに関する情報 (たとえば、作成された時刻) を格納することもできます。 そのような場合は、結合エンティティの型を定義し、この型に "関連付けペイロード" のプロパティを追加することをお勧めします。 また、多対多リレーションシップに使われる "スキップ ナビゲーション" に加えて、結合エンティティへのナビゲーションを作成することも一般的です。 これらの追加のナビゲーションを使うと、結合エンティティをコードから簡単に参照できるため、ペイロード データの読み取りや変更が容易になります。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public DateTime CreatedOn { get; set; }
}

また、生成された値をペイロード プロパティに対して使うことも一般的です。たとえば、関連付けの行の挿入時に自動的に設定されるデータベースのタイムスタンプなどです。 これには、最小限の構成が必要になります。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

結果はエンティティ型スキーマにマップされ、行の挿入時にタイムスタンプが自動的に設定されます。

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

ヒント

ここに示す SQL は SQLite 用です。 SQL Server や Azure SQL では、.HasDefaultValueSql("GETUTCDATE()") を使用し、TEXT の場合は、datetime を読み取ります。

結合エンティティとしてのカスタム共有型のエンティティ型

前の例では、結合エンティティ型として型 PostTag を使いました。 この型は、投稿とタグのリレーションシップに固有のものです。 ただし、同じ形を持つ複数の結合テーブルがある場合は、それらすべてに同じ CLR 型を使用できます。 たとえば、使用するすべての結合テーブルに CreatedOn 列があるとします。 共有型のエンティティ型としてマップされた JoinType クラスを使って、これらをマップできます。

public class JoinType
{
    public int Id1 { get; set; }
    public int Id2 { get; set; }
    public DateTime CreatedOn { get; set; }
}

その後、この型は、複数の異なる多対多リレーションシップによって、結合エンティティ型として参照できます。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<JoinType> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<JoinType> PostTags { get; } = [];
}

public class Blog
{
    public int Id { get; set; }
    public List<Author> Authors { get; } = [];
    public List<JoinType> BlogAuthors { get; } = [];
}

public class Author
{
    public int Id { get; set; }
    public List<Blog> Blogs { get; } = [];
    public List<JoinType> BlogAuthors { get; } = [];
}

次に、これらのリレーションシップを、リレーションシップごとに異なるテーブルに結合型をマップするよう適切に構成できます。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<JoinType>(
            "PostTag",
            l => l.HasOne<Tag>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id1),
            r => r.HasOne<Post>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));

    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Authors)
        .WithMany(e => e.Blogs)
        .UsingEntity<JoinType>(
            "BlogAuthor",
            l => l.HasOne<Author>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id1),
            r => r.HasOne<Blog>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

これにより、データベース スキーマに次のテーブルが作成されます。

CREATE TABLE "BlogAuthor" (
    "Id1" INTEGER NOT NULL,
    "Id2" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_BlogAuthor" PRIMARY KEY ("Id1", "Id2"),
    CONSTRAINT "FK_BlogAuthor_Authors_Id1" FOREIGN KEY ("Id1") REFERENCES "Authors" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_BlogAuthor_Blogs_Id2" FOREIGN KEY ("Id2") REFERENCES "Blogs" ("Id") ON DELETE CASCADE);


CREATE TABLE "PostTag" (
    "Id1" INTEGER NOT NULL,
    "Id2" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("Id1", "Id2"),
    CONSTRAINT "FK_PostTag_Posts_Id2" FOREIGN KEY ("Id2") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_Id1" FOREIGN KEY ("Id1") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

代替キーを使った多対多

これまでのすべての例では、結合エンティティ型の外部キーは、リレーションシップの両側にあるエンティティ型の主キーに制約されていました。 代わりに、各外部キー (またはその両方) を代替キーに制約することができます。 たとえば、TagPost が代替キー プロパティを持つ次のモデルを考えてみます。

public class Post
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Post> Posts { get; } = [];
}

このモデルの構成は次のようになります。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasPrincipalKey(nameof(Tag.AlternateKey)),
            r => r.HasOne(typeof(Post)).WithMany().HasPrincipalKey(nameof(Post.AlternateKey)));
}

わかりやすくするために、代替キーを含むテーブルも含めて、結果のデータベース スキーマを次に示します。

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

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "PostTag" (
    "PostsAlternateKey" INTEGER NOT NULL,
    "TagsAlternateKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsAlternateKey", "TagsAlternateKey"),
    CONSTRAINT "FK_PostTag_Posts_PostsAlternateKey" FOREIGN KEY ("PostsAlternateKey") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsAlternateKey" FOREIGN KEY ("TagsAlternateKey") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);

結合エンティティ型が .NET 型で表される場合、代替キーを使うための構成は若干異なります。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

これで、構成でジェネリック メソッド UsingEntity<> を使用できるようになりました。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey));
}

結果のスキーマは次のようになります。

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

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);

多対多と個別の主キーを持つ結合テーブル

これまでのすべての例の結合エンティティ型には、2 つの外部キー プロパティで構成された主キーがあります。 その理由は、これらのプロパティの値の各組み合わせが最大で 1 回だけ生じる可能性があるためです。 したがって、これらのプロパティは自然な主キーを形成します。

注意

EF Core では、コレクション ナビゲーションでの重複するエンティティはサポートされていません。

自分がデータベース スキーマを制御する場合、結合テーブルに主キー列を追加する理由はありません。ただし、既存の結合テーブルに主キー列が定義されている可能性はあります。 EF は、何らかの構成でこれにもマップできます。

その最も簡単な方法は、おそらく結合エンティティを表すクラスを作成することです。 次に例を示します。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int Id { get; set; }
    public int PostId { get; set; }
    public int TagId { get; set; }
}

この PostTag.Id プロパティは規則によって主キーとして取得されるようになったため、必要な構成は PostTag 型の UsingEntity の呼び出しのみです。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

結合テーブルの生成されるスキーマは次のようになります。

CREATE TABLE "PostTag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

主キーは、そのクラスを定義せずに結合エンティティに追加することもできます。 たとえば、Post 型と Tag 型のみを使う場合は、次のようになります。

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

キーは、次の構成で追加できます。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            j =>
            {
                j.IndexerProperty<int>("Id");
                j.HasKey("Id");
            });
}

これにより、個別の主キー列を持つ結合テーブルが作成されます。

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

連鎖削除を行わない多対多

上記のすべての例では、結合テーブルと多対多リレーションシップの 2 つの側の間に作成された外部キーは、連鎖削除の動作で作成されています。 これは、リレーションシップのいずれかの側のエンティティが削除されると、そのエンティティの結合テーブル内の行が自動的に削除されることを意味するため、非常に便利です。 つまり、あるエンティティが存在しなくなると、他のエンティティとのそのリレーションシップも存在しなくなります。

この動作を変更すると便利な場合を想像するのは難しいですが、必要であれば変更できます。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().OnDelete(DeleteBehavior.Restrict),
            r => r.HasOne(typeof(Post)).WithMany().OnDelete(DeleteBehavior.Restrict));
}

結合テーブルのデータベース スキーマは、外部キー制約に対して制限付き削除動作を使用します。

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 RESTRICT,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE RESTRICT);

自己参照の多対多

多対多リレーションシップの両端で同じエンティティ型を使うことができます。これは "自己参照" リレーションシップと呼ばれます。 次に例を示します。

public class Person
{
    public int Id { get; set; }
    public List<Person> Parents { get; } = [];
    public List<Person> Children { get; } = [];
}

これは PersonPerson という名前の結合テーブルにマップされ、両方の外部キーが再び People テーブルを指しています。

CREATE TABLE "PersonPerson" (
    "ChildrenId" INTEGER NOT NULL,
    "ParentsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PersonPerson" PRIMARY KEY ("ChildrenId", "ParentsId"),
    CONSTRAINT "FK_PersonPerson_People_ChildrenId" FOREIGN KEY ("ChildrenId") REFERENCES "People" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PersonPerson_People_ParentsId" FOREIGN KEY ("ParentsId") REFERENCES "People" ("Id") ON DELETE CASCADE);

対称的な自己参照の多対多

多対多リレーションシップが自然に対称的になる場合があります。 つまり、エンティティ A がエンティティ B に関連付けられている場合、エンティティ B もエンティティ A に関連付けられています。これは、1 つのナビゲーションを使って自然にモデル化されます。 たとえば、人物 A が人物 B と友人であれば、人物 B も人物 A と友人であるというケースを想像してみましょう。

public class Person
{
    public int Id { get; set; }
    public List<Person> Friends { get; } = [];
}

残念ながら、これをマップするのは簡単ではありません。 リレーションシップの両端に同じナビゲーションを使うことはできません。 実行できる最善の方法は、一方向の多対多リレーションシップとしてマップすることです。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasMany(e => e.Friends)
        .WithMany();
}

ただし、2 人の人物が互いに関連付けられるようにするには、それぞれの人物がもう 1 人の Friends コレクションに手動で追加される必要があります。 次に例を示します。

ginny.Friends.Add(hermione);
hermione.Friends.Add(ginny);

結合テーブルの直接使用

上記のすべての例では、EF Core の多対多マッピング パターンを使用しています。 ただし、結合テーブルを通常のエンティティ型にマップし、すべての操作に対して 2 つの一対多リレーションシップのみを使うこともできます。

たとえば、次のエンティティ型は、多対多リレーションシップを使わずに、2 つの通常のテーブルと結合テーブルのマッピングを表しています。

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = new();
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = new();
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

これらは通常の一対多リレーションシップを持つ通常のエンティティ型であるため、特別なマッピングは必要ありません。

その他の技術情報