最初に多対多のコードを作成し、関連テーブルにフィールドを追加します


298

私はこのシナリオを持っています:

public class Member
{
    public int MemberID { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual ICollection<Comment> Comments { get; set; }
}

public class Comment
{
    public int CommentID { get; set; }
    public string Message { get; set; }

    public virtual ICollection<Member> Members { get; set; }
}

public class MemberComment
{
    public int MemberID { get; set; }
    public int CommentID { get; set; }
    public int Something { get; set; }
    public string SomethingElse { get; set; }
}

Fluent APIとの関連付けを構成するにはどうすればよいですか?または、関連付けテーブルを作成するより良い方法はありますか?

回答:


524

カスタマイズされた結合テーブルを使用して多対多の関係を作成することはできません。多対多の関係では、EFは結合テーブルを内部的に管理し、非表示にします。これは、モデルにEntityクラスがないテーブルです。追加のプロパティを使用してこのような結合テーブルを操作するには、実際には2つの1対多の関係を作成する必要があります。次のようになります。

public class Member
{
    public int MemberID { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual ICollection<MemberComment> MemberComments { get; set; }
}

public class Comment
{
    public int CommentID { get; set; }
    public string Message { get; set; }

    public virtual ICollection<MemberComment> MemberComments { get; set; }
}

public class MemberComment
{
    [Key, Column(Order = 0)]
    public int MemberID { get; set; }
    [Key, Column(Order = 1)]
    public int CommentID { get; set; }

    public virtual Member Member { get; set; }
    public virtual Comment Comment { get; set; }

    public int Something { get; set; }
    public string SomethingElse { get; set; }
}

たとえば、LastName= "Smith"のメンバーのすべてのコメントを検索する場合は、次のようなクエリを記述できます。

var commentsOfMembers = context.Members
    .Where(m => m.LastName == "Smith")
    .SelectMany(m => m.MemberComments.Select(mc => mc.Comment))
    .ToList();

...または...

var commentsOfMembers = context.MemberComments
    .Where(mc => mc.Member.LastName == "Smith")
    .Select(mc => mc.Comment)
    .ToList();

または、「Smith」という名前のメンバーのリスト(複数あると想定)とコメントを一緒に作成するには、プロジェクションを使用できます。

var membersWithComments = context.Members
    .Where(m => m.LastName == "Smith")
    .Select(m => new
    {
        Member = m,
        Comments = m.MemberComments.Select(mc => mc.Comment)
    })
    .ToList();

MemberId= 1のメンバーのすべてのコメントを検索する場合:

var commentsOfMember = context.MemberComments
    .Where(mc => mc.MemberId == 1)
    .Select(mc => mc.Comment)
    .ToList();

結合テーブルのプロパティでフィルタリングすることもできます(多対多の関係では不可能です)。次に例を示しますSomething。プロパティに99を持つメンバー1のすべてのコメントをフィルタリングします。

var filteredCommentsOfMember = context.MemberComments
    .Where(mc => mc.MemberId == 1 && mc.Something == 99)
    .Select(mc => mc.Comment)
    .ToList();

遅延読み込みのため、物事が簡単になるかもしれません。ロードされているMember場合は、明示的なクエリなしでコメントを取得できるはずです。

var commentsOfMember = member.MemberComments.Select(mc => mc.Comment);

レイジーローディングは舞台裏で自動的にコメントをフェッチすると思います。

編集する

楽しみのために、このモデルでエンティティと関係を追加する方法と削除する方法の例をいくつか示します。

1)このメンバーの1つのメンバーと2つのコメントを作成します。

var member1 = new Member { FirstName = "Pete" };
var comment1 = new Comment { Message = "Good morning!" };
var comment2 = new Comment { Message = "Good evening!" };
var memberComment1 = new MemberComment { Member = member1, Comment = comment1,
                                         Something = 101 };
var memberComment2 = new MemberComment { Member = member1, Comment = comment2,
                                         Something = 102 };

context.MemberComments.Add(memberComment1); // will also add member1 and comment1
context.MemberComments.Add(memberComment2); // will also add comment2

context.SaveChanges();

2)member1の3番目のコメントを追加します。

var member1 = context.Members.Where(m => m.FirstName == "Pete")
    .SingleOrDefault();
if (member1 != null)
{
    var comment3 = new Comment { Message = "Good night!" };
    var memberComment3 = new MemberComment { Member = member1,
                                             Comment = comment3,
                                             Something = 103 };

    context.MemberComments.Add(memberComment3); // will also add comment3
    context.SaveChanges();
}

3)新しいメンバーを作成し、既存のコメントに関連付けます2:

var comment2 = context.Comments.Where(c => c.Message == "Good evening!")
    .SingleOrDefault();
if (comment2 != null)
{
    var member2 = new Member { FirstName = "Paul" };
    var memberComment4 = new MemberComment { Member = member2,
                                             Comment = comment2,
                                             Something = 201 };

    context.MemberComments.Add(memberComment4);
    context.SaveChanges();
}

4)既存のメンバー2とコメント3の間に関係を作成します。

var member2 = context.Members.Where(m => m.FirstName == "Paul")
    .SingleOrDefault();
var comment3 = context.Comments.Where(c => c.Message == "Good night!")
    .SingleOrDefault();
if (member2 != null && comment3 != null)
{
    var memberComment5 = new MemberComment { Member = member2,
                                             Comment = comment3,
                                             Something = 202 };

    context.MemberComments.Add(memberComment5);
    context.SaveChanges();
}

5)この関係をもう一度削除します。

var memberComment5 = context.MemberComments
    .Where(mc => mc.Member.FirstName == "Paul"
        && mc.Comment.Message == "Good night!")
    .SingleOrDefault();
if (memberComment5 != null)
{
    context.MemberComments.Remove(memberComment5);
    context.SaveChanges();
}

6)member1とそのコメントへのすべての関係を削除します。

var member1 = context.Members.Where(m => m.FirstName == "Pete")
    .SingleOrDefault();
if (member1 != null)
{
    context.Members.Remove(member1);
    context.SaveChanges();
}

これは中関係を削除MemberComments間の1対多の関係すぎているためMemberMemberCommentsの間Commentとは、MemberComments慣例により削除カスケード接続と設定されています。そして、これが当てはまるのは、in MemberIdCommentIdin MemberCommentMemberand Commentナビゲーションプロパティの外部キープロパティとして検出され、FKプロパティのタイプがnull不可でintあるため、最終的にcascading-delete-setupの原因となる関係が必要になるためです。このモデルでは理にかなっていると思います。


1
ありがとうございました。あなたが提供した追加情報に感謝します。
hgdean 2011

7
@hgdean:もう少しいくつかの例をスパムで送信しました。申し訳ありませんが、これは興味深いモデルであり、結合テーブルに追加データがある多対多についての質問が時々発生します。次回はリンクするものがあります... :)
Slauma

4
@Esteban:上書きされませんOnModelCreating。この例は、マッピング規則とデータ注釈のみに依存しています。
Slauma 2012

4
注:Fluent APIを使用せずにこのアプローチを使用する場合は、データベースにMemberIdCommentId列と列の複合キーのみがあり、追加の3番目の列Member_CommentId(またはそのようなもの)がないことを確認してください。つまり、正確に一致する名前がないことを意味します。キーのオブジェクト間
Simon_Weaver 2013年

3
@Simon_Weaver(または答えを知っている人)私も同様の状況ですが、そのテーブルの「MemberCommentID」主キーが欲しいのですが、これは可能ですか?私は現在の例外を取得しています、私は本当に助け...必要、私の質問をご覧くださいstackoverflow.com/questions/26783934/...
duxfox--

97

スラマによる素晴らしい答え。

Fluent APIマッピングを使用してこれを行うためのコードをポストするだけです。

public class User {
    public int UserID { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }

    public ICollection<UserEmail> UserEmails { get; set; }
}

public class Email {
    public int EmailID { get; set; }
    public string Address { get; set; }

    public ICollection<UserEmail> UserEmails { get; set; }
}

public class UserEmail {
    public int UserID { get; set; }
    public int EmailID { get; set; }
    public bool IsPrimary { get; set; }
}

あなたのDbContext派生クラスでこれを行うことができます:

public class MyContext : DbContext {
    protected override void OnModelCreating(DbModelBuilder builder) {
        // Primary keys
        builder.Entity<User>().HasKey(q => q.UserID);
        builder.Entity<Email>().HasKey(q => q.EmailID);
        builder.Entity<UserEmail>().HasKey(q => 
            new { 
                q.UserID, q.EmailID
            });

        // Relationships
        builder.Entity<UserEmail>()
            .HasRequired(t => t.Email)
            .WithMany(t => t.UserEmails)
            .HasForeignKey(t => t.EmailID)

        builder.Entity<UserEmail>()
            .HasRequired(t => t.User)
            .WithMany(t => t.UserEmails)
            .HasForeignKey(t => t.UserID)
    }
}

それはされていない別のアプローチで、受け入れ答えと同じ効果を持っているより良いも悪いこと。

編集: CreatedDateをboolからDateTimeに変更しました。

編集2:時間がないので、これが機能することを確認するために取り組んでいるアプリケーションの例を配置しました。


1
これは間違っていると思います。ここでは、両方のエンティティで1:Mである必要があるM:M関係を作成しています。
CHS 2013年

1
@CHS In your classes you can easily describe a many to many relationship with properties that point to each other.から取得:msdn.microsoft.com/en-us/data/hh134698.aspx。ジュリー・ラーマンは間違いありません。
エステバン2013年

1
エステバン、関係マッピングは本当に間違っています。@CHSはこれについて正しいです。Julie Lermanは「真の」多対多の関係について話していますが、ここでは多対多としてマッピングできないモデルの例を示しています。にCommentsプロパティがないため、マッピングはコンパイルされませんMember。また、エンティティにはの逆コレクションがないため、HasMany呼び出しの名前を変更してこれを修正することはできません。実際、正しいマッピングを取得するには、2つの1対多の関係を構成する必要があります。MemberCommentsMemberCommentWithMany
Slauma 2013年

2
ありがとうございました。私はこのソリューションに従って多対多のマッピングを行いました。
Thomas.Benz 2017年

私にはわかりませんが、これはMySqlでうまく機能します。ビルダーがないと、Mysqlは移行を試行したときにエラーをスローしました。
Rodrigo Prieto 2017

11

@Esteban、あなたが提供したコードは正しいです、ありがとう、しかし不完全です、私はそれをテストしました。「UserEmail」クラスに不足しているプロパティがあります:

    public UserTest UserTest { get; set; }
    public EmailTest EmailTest { get; set; }

誰かが興味を持っている場合は、テストしたコードを投稿します。よろしく

using System.Data.Entity;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Web;

#region example2
public class UserTest
{
    public int UserTestID { get; set; }
    public string UserTestname { get; set; }
    public string Password { get; set; }

    public ICollection<UserTestEmailTest> UserTestEmailTests { get; set; }

    public static void DoSomeTest(ApplicationDbContext context)
    {

        for (int i = 0; i < 5; i++)
        {
            var user = context.UserTest.Add(new UserTest() { UserTestname = "Test" + i });
            var address = context.EmailTest.Add(new EmailTest() { Address = "address@" + i });
        }
        context.SaveChanges();

        foreach (var user in context.UserTest.Include(t => t.UserTestEmailTests))
        {
            foreach (var address in context.EmailTest)
            {
                user.UserTestEmailTests.Add(new UserTestEmailTest() { UserTest = user, EmailTest = address, n1 = user.UserTestID, n2 = address.EmailTestID });
            }
        }
        context.SaveChanges();
    }
}

public class EmailTest
{
    public int EmailTestID { get; set; }
    public string Address { get; set; }

    public ICollection<UserTestEmailTest> UserTestEmailTests { get; set; }
}

public class UserTestEmailTest
{
    public int UserTestID { get; set; }
    public UserTest UserTest { get; set; }
    public int EmailTestID { get; set; }
    public EmailTest EmailTest { get; set; }
    public int n1 { get; set; }
    public int n2 { get; set; }


    //Call this code from ApplicationDbContext.ConfigureMapping
    //and add this lines as well:
    //public System.Data.Entity.DbSet<yournamespace.UserTest> UserTest { get; set; }
    //public System.Data.Entity.DbSet<yournamespace.EmailTest> EmailTest { get; set; }
    internal static void RelateFluent(System.Data.Entity.DbModelBuilder builder)
    {
        // Primary keys
        builder.Entity<UserTest>().HasKey(q => q.UserTestID);
        builder.Entity<EmailTest>().HasKey(q => q.EmailTestID);

        builder.Entity<UserTestEmailTest>().HasKey(q =>
            new
            {
                q.UserTestID,
                q.EmailTestID
            });

        // Relationships
        builder.Entity<UserTestEmailTest>()
            .HasRequired(t => t.EmailTest)
            .WithMany(t => t.UserTestEmailTests)
            .HasForeignKey(t => t.EmailTestID);

        builder.Entity<UserTestEmailTest>()
            .HasRequired(t => t.UserTest)
            .WithMany(t => t.UserTestEmailTests)
            .HasForeignKey(t => t.UserTestID);
    }
}
#endregion

3

多対多の構成の両方のフレーバーを実現できるソリューションを提案したいと思います。

「キャッチ」とは、EFがスキーマのテーブルが最大で1回にマッピングできることを検証するため、結合テーブルをターゲットとするビューを作成する必要があることですEntitySet

この回答は、以前の回答ですでに述べられていることを追加し、それらのアプローチのいずれもオーバーライドせず、それらに基づいています。

モデル:

public class Member
{
    public int MemberID { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual ICollection<Comment> Comments { get; set; }
    public virtual ICollection<MemberCommentView> MemberComments { get; set; }
}

public class Comment
{
    public int CommentID { get; set; }
    public string Message { get; set; }

    public virtual ICollection<Member> Members { get; set; }
    public virtual ICollection<MemberCommentView> MemberComments { get; set; }
}

public class MemberCommentView
{
    public int MemberID { get; set; }
    public int CommentID { get; set; }
    public int Something { get; set; }
    public string SomethingElse { get; set; }

    public virtual Member Member { get; set; }
    public virtual Comment Comment { get; set; }
}

構成:

using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;

public class MemberConfiguration : EntityTypeConfiguration<Member>
{
    public MemberConfiguration()
    {
        HasKey(x => x.MemberID);

        Property(x => x.MemberID).HasColumnType("int").IsRequired();
        Property(x => x.FirstName).HasColumnType("varchar(512)");
        Property(x => x.LastName).HasColumnType("varchar(512)")

        // configure many-to-many through internal EF EntitySet
        HasMany(s => s.Comments)
            .WithMany(c => c.Members)
            .Map(cs =>
            {
                cs.ToTable("MemberComment");
                cs.MapLeftKey("MemberID");
                cs.MapRightKey("CommentID");
            });
    }
}

public class CommentConfiguration : EntityTypeConfiguration<Comment>
{
    public CommentConfiguration()
    {
        HasKey(x => x.CommentID);

        Property(x => x.CommentID).HasColumnType("int").IsRequired();
        Property(x => x.Message).HasColumnType("varchar(max)");
    }
}

public class MemberCommentViewConfiguration : EntityTypeConfiguration<MemberCommentView>
{
    public MemberCommentViewConfiguration()
    {
        ToTable("MemberCommentView");
        HasKey(x => new { x.MemberID, x.CommentID });

        Property(x => x.MemberID).HasColumnType("int").IsRequired();
        Property(x => x.CommentID).HasColumnType("int").IsRequired();
        Property(x => x.Something).HasColumnType("int");
        Property(x => x.SomethingElse).HasColumnType("varchar(max)");

        // configure one-to-many targeting the Join Table view
        // making all of its properties available
        HasRequired(a => a.Member).WithMany(b => b.MemberComments);
        HasRequired(a => a.Comment).WithMany(b => b.MemberComments);
    }
}

コンテキスト:

using System.Data.Entity;

public class MyContext : DbContext
{
    public DbSet<Member> Members { get; set; }
    public DbSet<Comment> Comments { get; set; }
    public DbSet<MemberCommentView> MemberComments { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Configurations.Add(new MemberConfiguration());
        modelBuilder.Configurations.Add(new CommentConfiguration());
        modelBuilder.Configurations.Add(new MemberCommentViewConfiguration());

        OnModelCreatingPartial(modelBuilder);
     }
}

Saluma(@Saluma)の回答から

たとえば、LastName = "Smith"のメンバーのすべてのコメントを検索する場合は、次のようなクエリを記述できます。

これはまだ機能します...

var commentsOfMembers = context.Members
    .Where(m => m.LastName == "Smith")
    .SelectMany(m => m.MemberComments.Select(mc => mc.Comment))
    .ToList();

...しかし、今も...

var commentsOfMembers = context.Members
    .Where(m => m.LastName == "Smith")
    .SelectMany(m => m.Comments)
    .ToList();

または、「Smith」という名前のメンバーのリスト(複数あると想定)とコメントを一緒に作成するには、プロジェクションを使用できます。

これはまだ機能します...

var membersWithComments = context.Members
    .Where(m => m.LastName == "Smith")
    .Select(m => new
    {
        Member = m,
        Comments = m.MemberComments.Select(mc => mc.Comment)
    })
    .ToList();

...しかし、今も...

var membersWithComments = context.Members
    .Where(m => m.LastName == "Smith")
    .Select(m => new
    {
        Member = m,
        m.Comments
    })
        .ToList();

メンバーからコメントを削除したい場合

var comment = ... // assume comment from member John Smith
var member = ... // assume member John Smith

member.Comments.Remove(comment);

Include()メンバーのコメントを希望する場合

var member = context.Members
    .Where(m => m.FirstName == "John", m.LastName == "Smith")
    .Include(m => m.Comments);

これはすべてシンタックスシュガーのように感じますが、追加の構成を実行する場合は、いくつかの特典があります。どちらの方法でも、両方のアプローチを最大限に活用できるようです。


LINQクエリを入力するときの読みやすさの向上に感謝します。私はこの方法を採用しなければならないかもしれません。EF EntitySetもデータベースのビューを自動的に更新しますか?これはEF5.0計画で説明されている[Sugar]に似ていると思いますか?github.com/dotnet/EntityFramework.Docs/blob/master/...
Krptodr

なぜEntityTypeConfiguration<EntityType>エンティティタイプのキーとプロパティを再定義しているように見えるのでしょうか。たとえば、Property(x => x.MemberID).HasColumnType("int").IsRequired();は冗長であるようpublic int MemberID { get; set; }です。私の混乱している理解をクリアしていただけませんか?

0

TLDR; (EF6 / VS2012U5のEFエディターのバグに半関連)、DBからモデルを生成し、属性付きのm:mテーブルが表示されない場合:2つの関連テーブルを削除-> .edmxを保存->データベースから生成/追加- >保存します。

ここに来て、EF .edmxファイルに表示する属性列との多対多の関係を取得する方法(現在は表示されておらず、ナビゲーションプロパティのセットとして扱われているため)について疑問に思っていて、これらのクラスを生成した場合データベーステーブルから(または、MS用語ではデータベースファーストです)

.edmxで問題の2つのテーブル(OPの例では、メンバーとコメント)を削除し、[データベースからモデルを生成]を使用して再度追加します。(つまり、Visual Studioにそれらを更新させないでください-削除、保存、追加、保存)

次に、ここでの提案に沿って3番目のテーブルを作成します。

これは、純粋な多対多の関係が最初に追加され、属性が後でDBで設計される場合に関係します。

これは、このスレッド/グーグルからすぐには明らかになりませんでした。これは、問題を探しているがDB側から最初に来るGoogleのリンク#1であるため、そのまま公開します。


0

このエラーを解決する1つの方法は、 ForeignKey、外部キーとして必要なプロパティの上に属性、ナビゲーションプロパティを追加することです。

注:ForeignKey属性の括弧と二重引用符の間に、この方法で参照されるクラスの名前を入れます。

ここに画像の説明を入力してください


提供されたリンクが将来使用できなくなる可能性があるため、回答自体に最小限の説明を追加してください。
n4m31ess_c0d3r 2017

2
これは、クラスではなく、ナビゲーションプロパティの名前にする必要があります。
アーロン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.