Tạo mã trước, nhiều đến nhiều, với các trường bổ sung trong bảng kết hợp


297

Tôi có kịch bản này:

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

Làm cách nào để định cấu hình liên kết của tôi với API thông thạo ? Hoặc có cách nào tốt hơn để tạo bảng kết hợp?

Câu trả lời:


524

Không thể tạo mối quan hệ nhiều-nhiều với bảng tham gia tùy chỉnh. Trong mối quan hệ nhiều-nhiều, EF quản lý bảng tham gia nội bộ và ẩn. Đó là một bảng không có lớp Thực thể trong mô hình của bạn. Để làm việc với một bảng tham gia như vậy với các thuộc tính bổ sung, bạn sẽ phải tạo thực sự hai mối quan hệ một-nhiều. Nó có thể trông như thế này:

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

Nếu bây giờ bạn muốn tìm tất cả các nhận xét của các thành viên với LastName= "Smith", ví dụ bạn có thể viết một truy vấn như thế này:

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

... hoặc là ...

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

Hoặc để tạo danh sách các thành viên có tên "Smith" (chúng tôi giả sử có nhiều hơn một) cùng với nhận xét của họ, bạn có thể sử dụng phép chiếu:

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

Nếu bạn muốn tìm tất cả ý kiến ​​của một thành viên với MemberId= 1:

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

Giờ đây, bạn cũng có thể lọc theo các thuộc tính trong bảng tham gia của mình (điều này không thể thực hiện được trong mối quan hệ nhiều-nhiều), ví dụ: Lọc tất cả các nhận xét của thành viên 1 có thuộc tính 99 Something:

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

Bởi vì lười tải mọi thứ có thể trở nên dễ dàng hơn. Nếu bạn đã tải, Memberbạn sẽ có thể nhận được các bình luận mà không cần truy vấn rõ ràng:

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

Tôi đoán rằng tải lười biếng sẽ lấy các bình luận tự động đằng sau hậu trường.

Biên tập

Chỉ để cho vui một vài ví dụ nữa về cách thêm các thực thể và mối quan hệ và cách xóa chúng trong mô hình này:

1) Tạo một thành viên và hai bình luận của thành viên này:

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) Thêm nhận xét thứ ba của thành viên1:

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) Tạo thành viên mới và liên kết nó với nhận xét hiện có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) Tạo mối quan hệ giữa thành viên hiện tại2 và bình luận3:

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) Xóa mối quan hệ này một lần nữa:

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) Xóa thành viên1 và tất cả các mối quan hệ của nó với các bình luận:

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

Này sẽ xoá các mối quan hệ trong MemberCommentsquá vì các mối quan hệ một-nhiều giữa MemberMemberCommentsvà giữa CommentMemberCommentsđược thiết lập với tầng xóa theo quy ước. Và đây là trường hợp vì MemberIdCommentIdtrong MemberCommentđược phát hiện như là thuộc tính khóa ngoại cho MemberCommentchuyển hướng tài sản và kể từ khi các thuộc tính FK là loại không nullable intmối quan hệ là yêu cầu mà cuối cùng làm cho tầng-delete-setup. Làm cho ý nghĩa trong mô hình này, tôi nghĩ.


1
Cảm ơn bạn. Rất đánh giá cao các thông tin bổ sung mà bạn cung cấp.
hgdean

7
@hgdean: Tôi đã spam một vài ví dụ nữa, xin lỗi, nhưng đó là một mô hình thú vị và câu hỏi về nhiều-nhiều với dữ liệu bổ sung trong bảng tham gia xảy ra mỗi giờ và tại đây. Bây giờ, lần sau tôi có thứ gì đó để liên kết đến ... :)
Slauma

4
@Esteban: Không có ghi đè OnModelCreating. Ví dụ này chỉ dựa trên các quy ước ánh xạ và chú thích dữ liệu.
Slauma

4
Lưu ý: nếu bạn sử dụng phương pháp này mà không có API thông thạo, hãy đảm bảo rằng bạn kiểm tra cơ sở dữ liệu của mình rằng bạn chỉ có khóa tổng hợp MemberIdCommentIdcột chứ không phải cột thứ ba bổ sung Member_CommentId(hoặc đại loại như thế) - có nghĩa là bạn không có tên khớp chính xác trên các đối tượng cho khóa của bạn
Simon_Weaver

3
@Simon_Weaver (hoặc bất kỳ ai có thể biết câu trả lời) Tôi có một tình huống tương tự nhưng tôi muốn có khóa chính "MemberVerID" cho bảng đó, điều này có khả thi hay không? Tôi hiện đang có một ngoại lệ, vui lòng xem câu hỏi của tôi, tôi thực sự cần trợ giúp ... stackoverflow.com/questions/26783934/ chủ
duxfox--

97

Câu trả lời tuyệt vời của Slauma.

Tôi sẽ chỉ đăng mã để làm điều này bằng cách sử dụng ánh xạ API trôi chảy .

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

Trên DbContextlớp dẫn xuất của bạn, bạn có thể làm điều này:

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

Nó có tác dụng tương tự như câu trả lời được chấp nhận, với một cách tiếp cận khác, không tốt hơn cũng không tệ hơn.

EDIT: Tôi đã thay đổi createdDate từ bool thành DateTime.

EDIT 2: Do thiếu thời gian, tôi đã đặt một ví dụ từ một ứng dụng mà tôi đang làm việc để chắc chắn rằng nó hoạt động.


1
Tôi nghĩ rằng điều này là sai. Bạn đang tạo mối quan hệ M: M ở đây, nơi nó cần là 1: M cho cả hai thực thể.
CHS

1
@CHS In your classes you can easily describe a many to many relationship with properties that point to each other.lấy từ: msdn.microsoft.com/en-us/data/hh134698.aspx . Julie Lerman không thể sai được.
Esteban

1
Esteban, bản đồ mối quan hệ là thực sự không chính xác. @CHS nói đúng về điều này. Julie Lerman đang nói về một mối quan hệ nhiều-nhiều "thật" trong khi chúng ta có một ví dụ cho một mô hình không thể được ánh xạ như nhiều-nhiều. Bản đồ của bạn thậm chí sẽ không được biên dịch vì bạn không có Commentstài sản Member. Và bạn không thể sửa lỗi này bằng cách đổi tên HasManycuộc gọi thành MemberCommentsMemberCommentthực thể không có bộ sưu tập nghịch đảo WithMany. Trong thực tế, bạn cần cấu hình hai mối quan hệ một-nhiều để có được ánh xạ phù hợp.
Slauma

2
Cảm ơn bạn. Tôi đã làm theo giải pháp này để thực hiện ánh xạ nhiều-nhiều.
Thomas.Benz

Tôi không biết tôi nhưng điều này hoạt động tốt hơn với MySql. Không có trình xây dựng, Mysql sẽ gây ra lỗi cho tôi khi tôi thử di chuyển.
Rodrigo Prieto

11

@Esteban, mã bạn cung cấp là đúng, cảm ơn, nhưng không đầy đủ, tôi đã kiểm tra nó. Có các thuộc tính bị thiếu trong lớp "UserEmail":

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

Tôi đăng mã tôi đã kiểm tra nếu ai đó quan tâm. Trân trọng

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

Tôi muốn đề xuất một giải pháp trong đó cả hai hương vị của cấu hình nhiều-nhiều có thể đạt được.

"Bắt" là chúng ta cần tạo một khung nhìn nhắm vào Bảng tham gia, vì EF xác nhận rằng bảng của lược đồ có thể được ánh xạ nhiều nhất một lần mỗi lần EntitySet.

Câu trả lời này thêm vào những gì đã được nói trong các câu trả lời trước đó và không ghi đè lên bất kỳ phương pháp tiếp cận nào, nó dựa trên chúng.

Ngươi mâu:

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

Cấu hình:

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

Bối cảnh:

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

Từ câu trả lời của Saluma (@Saluma)

Nếu bây giờ bạn muốn tìm tất cả các bình luận của các thành viên với LastName = "Smith", ví dụ bạn có thể viết một truy vấn như thế này:

Điều này vẫn hoạt động ...

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

... nhưng giờ cũng có thể là ...

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

Hoặc để tạo danh sách các thành viên có tên "Smith" (chúng tôi giả sử có nhiều hơn một) cùng với nhận xét của họ, bạn có thể sử dụng phép chiếu:

Điều này vẫn hoạt động ...

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

... nhưng giờ cũng có thể là ...

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

Nếu bạn muốn xóa một bình luận từ một thành viên

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

member.Comments.Remove(comment);

Nếu bạn muốn Include()bình luận của một thành viên

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

Tất cả điều này cảm thấy giống như đường cú pháp, tuy nhiên nó sẽ giúp bạn có được một vài đặc quyền nếu bạn sẵn sàng trải qua cấu hình bổ sung. Dù bằng cách nào bạn dường như có thể có được tốt nhất của cả hai phương pháp.


Tôi đánh giá cao khả năng đọc tăng lên khi gõ các truy vấn LINQ. Tôi có thể phải áp dụng phương pháp này. Tôi phải hỏi, liệu Entityset của EF có tự động cập nhật chế độ xem trong cơ sở dữ liệu không? Bạn có đồng ý điều này có vẻ giống với [Sugar] như được mô tả trong kế hoạch EF5.0 không? github.com/dotnet/EntityFramework.Docs/blob/master/
triệt

Tôi đang tự hỏi tại sao bạn dường như xác định lại trong EntityTypeConfiguration<EntityType>khóa và thuộc tính của loại thực thể. Eg Property(x => x.MemberID).HasColumnType("int").IsRequired();dường như là dư thừa với public int MemberID { get; set; }. Bạn có thể xóa sự hiểu biết khó hiểu của tôi xin vui lòng?
phút

0

TLDR; (bán liên quan đến lỗi trình soạn thảo EF trong EF6 / VS2012U5) nếu bạn tạo mô hình từ DB và bạn không thể thấy bảng m: m được gán: Xóa hai bảng có liên quan -> Lưu .edmx -> Tạo / thêm từ cơ sở dữ liệu - > Lưu lại.

Đối với những người đến đây tự hỏi làm thế nào để có được mối quan hệ nhiều-nhiều với các cột thuộc tính để hiển thị trong tệp .edmx của EF (vì hiện tại nó sẽ không hiển thị và được coi là một tập hợp các thuộc tính điều hướng), VÀ bạn đã tạo các lớp này từ bảng cơ sở dữ liệu của bạn (hoặc cơ sở dữ liệu đầu tiên trong MS lingo, tôi tin thế.)

Xóa 2 bảng trong câu hỏi (để lấy ví dụ OP, Thành viên và Nhận xét) trong .edmx của bạn và thêm lại chúng thông qua 'Tạo mô hình từ cơ sở dữ liệu'. (tức là không cố gắng để Visual Studio cập nhật chúng - xóa, lưu, thêm, lưu)

Sau đó, nó sẽ tạo một bảng thứ 3 phù hợp với những gì được đề xuất ở đây.

Điều này có liên quan trong trường hợp ban đầu một mối quan hệ nhiều-nhiều thuần túy được thêm vào và các thuộc tính được thiết kế trong DB sau này.

Điều này không ngay lập tức rõ ràng từ chủ đề này / Googling. Vì vậy, chỉ cần đưa nó ra ngoài vì đây là liên kết số 1 trên Google để tìm kiếm vấn đề nhưng đến từ phía DB trước.


0

Một cách để giải quyết lỗi này là đặt ForeignKeythuộc tính lên trên thuộc tính bạn muốn làm khóa ngoại và thêm thuộc tính điều hướng.

Lưu ý: Trong ForeignKeythuộc tính, giữa dấu ngoặc đơn và dấu ngoặc kép, đặt tên của lớp được đề cập theo cách này.

nhập mô tả hình ảnh ở đây


Vui lòng thêm một lời giải thích tối thiểu trong chính câu trả lời, vì liên kết được cung cấp có thể xảy ra không có sẵn trong tương lai.
n4m31ess_c0d3r

2
Nó phải là tên của thuộc tính điều hướng , chứ không phải là lớp.
aaron
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.