我有这样一个场景:

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的关联?或者是否有更好的方法来创建关联表?


当前回答

我现在已经回到这里几次了,但似乎EF核心已经在过去的十年里做了一些更新,所以这是我目前在哪里与自定义连接实体设置多对多:

public class MemberModel
{
    public int MemberId { get; set; }

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

    public ICollection<CommentModel> Comments { get; set; }
}

public class CommentModel
{
    public int CommentId { get; set; }
    public string Message { get; set; }

    public ICollection<MemberModel> Members { get; set; }
}

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

    public int MembersId { get; set; }
    [ForeignKey("MembersId")]
    public MemberModel Member { get; set; }

    public int CommentsId { get; set; }
    [ForeignKey("CommentsId")]
    public CommentModel Comment { get; set; }
}

然后在OnModelCreating中:

//Allows access directly from Comments or Members entities to the other
builder.Entity<MemberModel>()
    .HasMany(x => x.Comments)
    .WithMany(x => x.Members)
    .UsingEntity<MemberCommentModel>();

//Defines the actual relationships for the middle table
builder.Entity<MemberCommentModel>()
    .HasOne(x => x.Comment)
    .WithOne()
    .OnDelete(DeleteBehavior.NoAction);
builder.Entity<MemberCommentModel>()
    .HasOne(x => x.Member)
    .WithOne()
    .OnDelete(DeleteBehavior.NoAction);

其他回答

我想提出一种可以同时实现多对多配置的解决方案。

“问题”是我们需要创建一个以连接表为目标的视图,因为EF验证了每个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)的回答

如果您现在想要找到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);

这一切都感觉像是语法糖,但是,如果您愿意进行额外的配置,它确实会给您带来一些好处。无论哪种方式,您似乎都能够获得两种方法的最佳效果。

解决这个错误的一种方法是将ForeignKey属性放在你想要作为外键的属性之上,并添加navigation属性。

注意:在ForeignKey属性中,在括号和双引号之间,放置以这种方式引用的类的名称。

TLDR;(与EF6/VS2012U5中的EF编辑器错误半相关)如果你从DB生成模型,你不能看到有属性的m:m表:删除两个相关的表->保存。edmx ->从数据库生成/添加->保存。

对于那些想知道如何在EF .edmx文件中显示属性列的多对多关系的人(因为它目前不会显示并被视为一组导航属性),并且您从数据库表(或者用MS行话来说是数据库优先)生成了这些类。

删除。edmx中的两个表(以OP为例,Member和Comment),并通过“从数据库生成模型”再次添加它们。(即不要试图让Visual Studio更新它们-删除,保存,添加,保存)

然后,它将根据这里的建议创建第三个表。

这与先添加纯多对多关系,然后在DB中设计属性的情况有关。

从这个帖子/谷歌上看,这个问题并没有立即得到解决。所以只是把它放在那里,因为这是谷歌上的链接#1,寻找问题,但首先来自DB方面。

不可能使用自定义连接表创建多对多关系。在多对多关系中,EF在内部和隐藏地管理连接表。它是一个在模型中没有Entity类的表。要使用这样一个具有其他属性的连接表,实际上必须创建两个一对多的关系。它可能是这样的:

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();

现在你还可以通过连接表中的属性进行过滤(在多对多关系中是不可能的),例如:过滤成员1中属性为99的所有注释

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

因为惰性加载,事情可能会变得更容易。如果你有一个加载的成员,你应该能够在没有显式查询的情况下获得注释:

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

我猜延迟加载会在幕后自动获取注释。

Edit

只是为了好玩一些例子,更多的如何添加实体和关系,以及如何删除他们在这个模型:

1)创建一个成员和该成员的两个注释:

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的第三条评论:

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)创建新成员,并将其与现有的评论联系起来。

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)在现有的member2和comment3之间创建关系:

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();
}

This deletes the relationships in MemberComments too because the one-to-many relationships between Member and MemberComments and between Comment and MemberComments are setup with cascading delete by convention. And this is the case because MemberId and CommentId in MemberComment are detected as foreign key properties for the Member and Comment navigation properties and since the FK properties are of type non-nullable int the relationship is required which finally causes the cascading-delete-setup. Makes sense in this model, I think.

我将发布使用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)
    }
}

它和公认的答案有同样的效果,只是方法不同,没有更好也没有更坏。