首先创建代码,多对多,关联表中的附加字段

Posted

技术标签:

【中文标题】首先创建代码,多对多,关联表中的附加字段【英文标题】:Create code first, many to many, with additional fields in association table 【发布时间】:2011-10-26 09:31:08 【问题描述】:

我有这种情况:

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

【问题讨论】:

【参考方案1】:

无法使用自定义连接表创建多对多关系。在多对多关系中,EF 在内部管理并隐藏连接表。这是一个在您的模型中没有实体类的表。要使用具有附加属性的这种连接表,您实际上必须创建两个一对多关系。它可能看起来像这样:

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" 的成员的 cmets,您可以编写如下查询:

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”的成员列表(我们假设有多个)以及他们的 cmets,您可以使用投影:

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

如果要查找MemberId = 1 的成员的所有cmets:

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

现在您还可以按连接表中的属性进行过滤(这在多对多关系中是不可能的),例如:过滤成员 1 的所有 cmets,其属性 Something 中的值为 99:

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

由于延迟加载,事情可能会变得更容易。如果您有一个已加载的Member,您应该能够在没有显式查询的情况下获取 cmets:

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

我猜延迟加载会在后台自动获取 cmets。

编辑

为了好玩,再举几个例子,在这个模型中如何添加实体和关系以及如何删除它们:

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

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 及其与 cmets 的所有关系:

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

    context.Members.Remove(member1);
    context.SaveChanges();

这也删除了MemberComments 中的关系,因为MemberMemberComments 之间以及CommentMemberComments 之间的一对多关系按照惯例设置了级联删除。之所以出现这种情况,是因为MemberComment 中的MemberIdCommentId 被检测为MemberComment 导航属性的外键属性,并且由于FK 属性的类型是不可为空的int 关系是必需的,最终导致级联删除设置。我认为在这个模型中是有意义的。

【讨论】:

谢谢。非常感谢您提供的其他信息。 @hgdean:对不起,我已经发送了更多示例,但这是一个有趣的模型,关于连接表中的附加数据的多对多问题时不时出现。现在下次我要链接到... :) @Esteban:没有被覆盖的OnModelCreating。该示例仅依赖于映射约定和数据注释。 注意:如果您在没有 Fluent API 的情况下使用此方法,请确保您在数据库中检查您只有一个包含 MemberIdCommentId 列的组合键,而不是额外的第三列 Member_CommentId (或类似的东西) - 这意味着您的键的对象之间没有完全匹配的名称 @Simon_Weaver(或任何可能知道答案的人)我有类似的情况,但我想拥有该表的“MemberCommentID”主键,这可能吗?我目前遇到异常,请看我的问题,我真的需要帮助...***.com/questions/26783934/…【参考方案2】:

我将发布代码以使用流畅的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)
    

它和the accepted answer 的效果相同,但方法不同,没有好也不坏。

【讨论】:

我认为这是错误的。您在此处创建 M:M 关系,两个实体的关系必须为 1:M。 @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。朱莉·勒曼不会错。 Esteban,关系映射真的不正确。 @CHS 对此是正确的。 Julie Lerman 正在谈论“真正的”多对多关系,而我们这里有一个无法映射为多对多的模型示例。您的映射甚至无法编译,因为您在 Member 中没有 Comments 属性。而且您不能通过将HasMany 调用重命名为MemberComments 来解决此问题,因为MemberComment 实体没有WithMany 的反向集合。事实上,您需要配置两个一对多关系才能获得正确的映射。 谢谢。我按照这个解决方案进行多对多映射。 我不知道,但这对 mysql 更有效。如果没有构建器,当我尝试迁移时,Mysql 会抛出一个错误。【参考方案3】:

code provided by this answer 是对的,但不完整,我已经测试过了。 “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

【讨论】:

【参考方案4】:

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

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

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

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

这适用于首先添加纯多对多关系,然后在数据库中设计属性的情况。

此线程/谷歌搜索并未立即明确这一点。因此,只需将其放在那里,因为这是 Google 上的链接 #1,正在寻找问题,但首先来自数据库方面。

【讨论】:

【参考方案5】:

解决此错误的一种方法是将ForeignKey 属性放在您想要作为外键的属性的顶部并添加导航属性。

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

【讨论】:

请在答案中添加一个简单的解释,因为提供的链接将来可能会不可用。 应该是导航属性的名称,而不是类。【参考方案6】:

我想提出一个解决方案,可以实现多对多配置的两种风格。

“关键”是我们需要创建一个以联接表为目标的视图,因为 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) answer

如果您现在想要查找所有姓氏 = 的成员的 cmets 例如“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”的成员列表(我们假设有 不止一个)连同他们的 cmets,您可以使用投影:

这仍然有效...

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()一个会员的cmets

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/… 我想知道为什么您似乎在EntityTypeConfiguration&lt;EntityType&gt; 中重新定义了实体类型的键和属性。例如。 Property(x =&gt; x.MemberID).HasColumnType("int").IsRequired(); 似乎与 public int MemberID get; set; 是多余的。你能解释一下我令人困惑的理解吗?

以上是关于首先创建代码,多对多,关联表中的附加字段的主要内容,如果未能解决你的问题,请参考以下文章

Android Room - 带有附加字段的多对多关系

与 Shopware 6 中的其他字段的多对多关联

多对多关联表PK作为其他表中的FK

Hibernate多表关系配置——多对多对关系映射

如何查询和改变表中的“多对多”字段?

laravel多对多关联模型