次の方法で共有


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

多対多リレーションシップは、1 つのエンティティ型の任意の数のエンティティが、同じエンティティ型または別のエンティティ型の任意の数のエンティティに関連付けられている場合に使用されます。 たとえば、 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 は結合テーブルです。 これには、PostsId テーブルの主キーの外部キーである Posts と、TagsId テーブルの主キーの外部キーである Tags の 2 つの列が含まれます。 したがって、このテーブルの各行は、1 つの Post と 1 つの Tagの間の関連付けを表します。

EF Core でのこのスキーマの単純なマッピングは、テーブルごとに 1 つずつ、3 つのエンティティ型で構成されます。 これらの各エンティティ型が .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!;
}

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

EF では、関連するPostを含む 2 つのコレクション ナビゲーション (Tags上の 1 つと、関連するTagを含むPostsの逆) の導入により、より自然なマッピングが可能になります。 例えば次が挙げられます。

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",
            r => r.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagsId").HasPrincipalKey(nameof(Tag.Id)),
            l => l.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);

ヒント

データベースファーストフローを使用して 既存のデータベースから 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(
            r => r.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagForeignKey"),
            l => l.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 つの例を組み合わせて、結合テーブル名とその外部キー列名をマップ変更できます。

結合エンティティのクラスを使用する多対多

ここまでの例では、結合テーブルは 共有型エンティティ型に自動的にマップされています。 これにより、エンティティ型の専用クラスを作成する必要がなくなります。 ただし、ナビゲーションやペイロード ("ペイロード" が結合テーブル内の追加データである場合は特に) 簡単に参照できるように、このようなクラスを用意すると便利です。たとえば、結合テーブル内のエントリが作成されるタイムスタンプなど)。は、次の後の例に示すように、クラスに追加されます。 これを行うには、まず、PostTagPostの既存の型に加えて、結合エンティティの型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; } = [];
}

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>(
            r => r.HasOne<Tag>().WithMany().HasForeignKey(e => e.TagId),
            l => l.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; }
}

重要

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

結合エンティティへのナビゲーションは規則によって取得されるため、 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>(
            r => r.HasOne<Tag>().WithMany(e => e.PostTags),
            l => l.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>(
            r => r.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags),
            l => l.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>(
            r => r.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasForeignKey(e => e.TagForeignKey),
            l => l.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);

一方向の多対多

多対多リレーションシップの両側にナビゲーションを含める必要はありません。 例えば次が挙げられます。

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 型を使用しました。 この型は、posts-tags リレーションシップに固有です。 ただし、同じ図形を持つ複数の結合テーブルがある場合は、それらのすべてに同じ 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",
            r => r.HasOne<Tag>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id1),
            l => l.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",
            r => r.HasOne<Author>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id1),
            l => l.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(
            r => r.HasOne(typeof(Tag)).WithMany().HasPrincipalKey(nameof(Tag.AlternateKey)),
            l => l.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>(
            r => r.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey),
            l => l.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プロパティは規則によって主キーとして取得されるようになったため、必要な構成は、UsingEntity型のPostTagの呼び出しだけです。

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(
            r => r.HasOne(typeof(Tag)).WithMany().OnDelete(DeleteBehavior.Restrict),
            l => l.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 人が互いに関連していることを確認するには、各ユーザーを他のユーザーの 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!;
}

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

その他のリソース