首先创建代码,多对多创建,并在关联表中添加其他字段


297

我有这种情况:

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配置关联?还是有更好的方法来创建关联表?

Answers:


524

用自定义的联接表创建多对多关系是不可能的。在多对多关系中,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的所有注释Something

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)创建一个成员和该成员的两个评论:

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)创建新成员并将其与现有评论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)在现有的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();
}

该删除的关系MemberComments也因为之间的一个一对多的关系MemberMemberComments与之间CommentMemberComments是建立与惯例级联删除。这是因为该情况MemberId,并CommentIdMemberComment被检测为外键的属性MemberComment导航性能,因为FK属性的类型非空的int,需要的关系,并最终导致级联,删除,设置。我认为在这种模型中是有道理的。


1
谢谢。非常感谢您提供的其他信息。
hgdean 2011年

7
@hgdean:很抱歉,我发送了一些其他示例,但这是一个有趣的模型,有关连接表中多对多数据的问题不时出现。现在,下一次我可以链接到... :)
Slauma 2011年

4
@Esteban:没有覆盖OnModelCreating。该示例仅依赖于映射约定和数据注释。
Slauma 2012年

4
注意:如果您在不使用Fluent API的情况下使用此方法,请确保您在数据库中检查到您仅具有带有MemberIdCommentId列的组合键,而没有额外的第三列Member_CommentId(或类似的东西)-这意味着您没有完全匹配的名称跨对象作为钥匙
Simon_Weaver 2013年

3
@Simon_Weaver(或可能知道答案的任何人)我也遇到类似的情况,但是我希望该表具有“ MemberCommentID”主键,这可能吗?目前,我得到一个例外,请看看我的问题,我真的需要帮助...... stackoverflow.com/questions/26783934/...
duxfox--

97

Slauma的出色回答。

我将使用流畅的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
我认为这是错误的。您正在此处创建M:M关系,两个实体的关系都必须为1:M。
CHS

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。朱莉·勒曼(Julie Lerman)没错。
埃斯特万(Esteban)

1
Esteban,关系映射确实不正确。@CHS是正确的。朱莉·勒曼(Julie Lerman)谈论的是“真正的”多对多关系,而我们这里有一个示例,该模型无法映射为多对多。您的映射甚至不会编译,因为您在中没有Comments属性Member。而且您不能只是通过将HasMany调用重命名来解决此问题,MemberComments因为该MemberComment实体没有的逆集合WithMany。实际上,您需要配置两个一对多关系才能获得正确的映射。
Slauma

2
谢谢。我遵循此解决方案进行了多对多映射。
Thomas.Benz

我不知道,但是MySql可以更好地工作。如果没有构建器,当尝试迁移时,Mysql会向我抛出错误。
罗德里戈·普列托

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验证了一个架构的表最多可以每个映射一次 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计划中描述的[糖]相似?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编辑器错误有关)如果您是从数据库生成模型的,而看不到属性的m:m表:删除两个相关表->保存.edmx->从数据库生成/添加- >保存。

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

在.edmx中删除有问题的2个表(以OP为例,成员和注释),然后通过“从数据库生成模型”再次添加它们。(即不要尝试让Visual Studio更新它们-删除,保存,添加,保存)

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

在首先添加纯多对多关系,然后在DB中设计属性的情况下,这是相关的。

从该线程/谷歌搜索尚不清楚。因此,只需将其放在那里,因为这是Google上的链接#1寻找问题,但首先来自数据库方面。


0

解决此错误的一种方法是将ForeignKey属性放在要作为外键的属性之上,然后添加导航属性。

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

在此处输入图片说明


请在答案中添加一个简短的说明,因为将来提供的链接可能会不可用。
n4m31ess_c0d3r

2
它应该是导航属性的名称,而不是类。
亚伦
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.