Buat kode terlebih dahulu, banyak ke banyak, dengan bidang tambahan di tabel asosiasi

298

Saya punya skenario ini:

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

Bagaimana cara mengkonfigurasi keterkaitan saya dengan API yang lancar ? Atau adakah cara yang lebih baik untuk membuat tabel asosiasi?

hgdean
sumber

Jawaban:

524

Tidak mungkin untuk membuat hubungan banyak-ke-banyak dengan tabel bergabung khusus. Dalam hubungan banyak-ke-banyak, EF mengelola tabel bergabung secara internal dan tersembunyi. Ini adalah tabel tanpa kelas Entity di model Anda. Untuk bekerja dengan tabel gabungan seperti itu dengan properti tambahan, Anda harus membuat sebenarnya dua hubungan satu-ke-banyak. Itu bisa terlihat seperti ini:

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

Jika sekarang Anda ingin menemukan semua komentar anggota dengan LastName= "Smith" misalnya, Anda dapat menulis kueri seperti ini:

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

... atau ...

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

Atau untuk membuat daftar anggota dengan nama "Smith" (kami menganggap ada lebih dari satu) bersama dengan komentar mereka, Anda dapat menggunakan proyeksi:

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

Jika Anda ingin menemukan semua komentar anggota dengan MemberId= 1:

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

Sekarang Anda juga dapat memfilter menurut properti di tabel bergabung Anda (yang tidak mungkin terjadi dalam hubungan banyak ke banyak), misalnya: Saring semua komentar anggota 1 yang memiliki 99 properti Something:

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

Karena memuat malas mungkin menjadi lebih mudah. Jika Anda telah memuat MemberAnda harus bisa mendapatkan komentar tanpa permintaan eksplisit:

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

Saya kira pemuatan malas akan mengambil komentar secara otomatis di belakang layar.

Edit

Hanya untuk bersenang-senang beberapa contoh lebih banyak cara menambahkan entitas dan hubungan dan cara menghapusnya dalam model ini:

1) Buat satu anggota dan dua komentar dari anggota ini:

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) Tambahkan komentar ketiga 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) Buat anggota baru dan hubungkan dengan komentar2 yang ada:

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) Buat hubungan antara member2 yang ada dan 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) Hapus hubungan ini lagi:

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) Hapus member1 dan semua hubungannya dengan komentar:

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

Ini juga menghapus hubungan MemberCommentskarena hubungan satu-ke-banyak antara Memberdan MemberCommentsdan di antara Commentdan MemberCommentsdiatur dengan penghapusan cascading oleh konvensi. Dan ini terjadi karena MemberIddan CommentIddalam MemberCommentterdeteksi sebagai properti kunci asing untuk Memberdan Commentnavigasi properti dan karena properti FK bertipe non-nullable inthubungan diperlukan yang akhirnya menyebabkan cascading-delete-setup. Masuk akal dalam model ini, saya pikir.

Slauma
sumber
1
Terima kasih. Sangat menghargai informasi tambahan yang Anda berikan.
hgdean
7
@hgdean: Saya telah memberikan beberapa contoh lagi, maaf, tetapi ini adalah model yang menarik dan pertanyaan tentang banyak-ke-banyak dengan data tambahan di tabel bergabung terjadi sekarang dan kemudian di sini. Sekarang untuk waktu berikutnya saya punya sesuatu untuk dihubungkan ... :)
Slauma
4
@Esteban: Tidak ada yang diganti OnModelCreating. Contoh ini hanya bergantung pada konvensi pemetaan dan anotasi data.
Slauma
4
Catatan: jika Anda menggunakan pendekatan ini tanpa API Lancar pastikan Anda memeriksa di database Anda bahwa Anda hanya memiliki kunci komposit dengan MemberIddan CommentIdkolom dan bukan kolom ketiga tambahan Member_CommentId(atau sesuatu seperti itu) - yang berarti Anda tidak memiliki nama yang cocok persis di seberang objek untuk kunci Anda
Simon_Weaver
3
@Simon_Weaver (atau siapa pun yang mungkin tahu jawabannya) Saya memiliki situasi yang sama tetapi saya ingin memiliki kunci utama "MemberCommentID" untuk tabel itu, apakah ini mungkin atau tidak? Saya saat ini mendapatkan pengecualian, silakan lihat pertanyaan saya, saya benar-benar membutuhkan bantuan ... stackoverflow.com/questions/26783934/
duxfox--
97

Jawaban yang sangat bagus dari Slauma.

Saya hanya akan memposting kode untuk melakukan ini menggunakan pemetaan API yang lancar .

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

Di DbContextkelas turunan Anda, Anda bisa melakukan ini:

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

Ini memiliki efek yang sama dengan jawaban yang diterima, dengan pendekatan yang berbeda, yang tidak lebih baik atau lebih buruk.

EDIT: Saya telah mengubah CreatedDate dari bool ke DateTime.

EDIT 2: Karena kurangnya waktu saya telah menempatkan contoh dari aplikasi yang saya kerjakan untuk memastikan ini berfungsi.

Esteban
sumber
1
Saya pikir ini salah. Anda membuat hubungan M: M di sini yang mana harus 1: M untuk kedua entitas.
CHS
1
@CHS In your classes you can easily describe a many to many relationship with properties that point to each other.diambil dari: msdn.microsoft.com/en-us/data/hh134698.aspx . Julie Lerman tidak mungkin salah.
Esteban
1
Esteban, pemetaan hubungan benar-benar salah. @CHS benar tentang ini. Julie Lerman berbicara tentang hubungan banyak-ke-banyak yang "benar", sementara di sini kita memiliki contoh untuk model yang tidak dapat dipetakan sebanyak-ke-banyak. Pemetaan Anda bahkan tidak dapat dikompilasi karena Anda tidak memiliki Commentsproperti di Member. Dan Anda tidak bisa memperbaikinya dengan mengganti nama HasManypanggilan menjadi MemberCommentskarena MemberCommententitas tidak memiliki koleksi terbalik untuk WithMany. Sebenarnya Anda perlu mengonfigurasi dua hubungan satu-ke-banyak untuk mendapatkan pemetaan yang tepat.
Slauma
2
Terima kasih. Saya mengikuti solusi ini untuk melakukan pemetaan banyak ke banyak.
Thomas.Benz
Saya tidak tahu, tapi ini berfungsi lebih baik dengan MySql. Tanpa pembangun, Mysql membuat saya kesalahan ketika saya mencoba migrasi.
Rodrigo Prieto
11

@Esteban, kode yang Anda berikan benar, terima kasih, tetapi tidak lengkap, saya sudah mengujinya. Ada properti yang hilang di kelas "UserEmail":

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

Saya memposting kode yang telah saya uji jika seseorang tertarik. Salam

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
LeonardoX
sumber
3

Saya ingin mengusulkan solusi di mana kedua rasa konfigurasi banyak-ke-banyak dapat dicapai.

"Tangkapan" adalah kita perlu membuat tampilan yang menargetkan Tabel Bergabung, karena EF memvalidasi bahwa tabel skema mungkin dipetakan paling banyak satu kali per EntitySet.

Jawaban ini menambah apa yang sudah dikatakan dalam jawaban sebelumnya dan tidak mengesampingkan salah satu dari pendekatan itu, itu dibangun di atas mereka.

Model:

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

Konfigurasi:

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

Isi:

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

Dari (@Saluma) Saluma ini jawabannya

Jika sekarang Anda ingin menemukan semua komentar anggota dengan LastName = "Smith" misalnya, Anda dapat menulis kueri seperti ini:

Ini masih berfungsi ...

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

... tapi sekarang juga bisa ...

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

Atau untuk membuat daftar anggota dengan nama "Smith" (kami menganggap ada lebih dari satu) bersama dengan komentar mereka, Anda dapat menggunakan proyeksi:

Ini masih berfungsi ...

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

... tapi sekarang juga bisa ...

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

Jika Anda ingin menghapus komentar dari anggota

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

member.Comments.Remove(comment);

Jika Anda ingin Include()komentar anggota

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

Ini semua terasa seperti gula sintaksis, tetapi itu memberi Anda beberapa tunjangan jika Anda mau melalui konfigurasi tambahan. Either way Anda tampaknya bisa mendapatkan yang terbaik dari kedua pendekatan.

Mauricio Morales
sumber
Saya menghargai peningkatan keterbacaan saat mengetik kueri LINQ. Saya mungkin harus mengadopsi metode ini. Saya harus bertanya, apakah EF EntitySet juga memperbarui tampilan dalam basis data secara otomatis? Apakah Anda setuju ini tampaknya mirip dengan [Gula] seperti yang dijelaskan dalam paket EF5.0? github.com/dotnet/EntityFramework.Docs/blob/master/…
Krptodr
Saya bertanya-tanya mengapa Anda sepertinya mendefinisikan kembali EntityTypeConfiguration<EntityType>kunci dan properti dari tipe entitas. Misalnya Property(x => x.MemberID).HasColumnType("int").IsRequired();sepertinya berlebihan public int MemberID { get; set; }. Bisakah Anda menghapus pengertian saya yang membingungkan?
mnt
0

TLDR; (semi-terkait dengan bug editor EF di EF6 / VS2012U5) jika Anda menghasilkan model dari DB dan Anda tidak dapat melihat tabel m: m: Hapus dua tabel terkait -> Simpan .edmx -> Hasilkan / tambah dari basis data - > Simpan.

Bagi mereka yang datang ke sini bertanya-tanya bagaimana mendapatkan hubungan banyak-ke-banyak dengan kolom atribut untuk ditampilkan dalam file EF .edmx (karena saat ini tidak akan ditampilkan dan diperlakukan sebagai seperangkat properti navigasi), DAN Anda membuat kelas-kelas ini dari tabel basis data Anda (atau basis data-pertama dalam istilah MS, saya percaya.)

Hapus 2 tabel yang dimaksud (untuk mengambil contoh OP, Anggota dan Komentar) di .edmx Anda dan tambahkan lagi melalui 'Buat model dari basis data'. (yaitu jangan mencoba untuk membiarkan Visual Studio memperbaruinya - menghapus, menyimpan, menambah, menyimpan)

Ini kemudian akan membuat tabel ke-3 sesuai dengan apa yang disarankan di sini.

Ini relevan dalam kasus di mana hubungan murni banyak-ke-banyak ditambahkan pada awalnya, dan atribut dirancang dalam DB nanti.

Ini tidak segera jelas dari utas ini / Googling. Jadi, letakkan di sana karena ini adalah tautan # 1 di Google yang mencari masalah tetapi berasal dari sisi DB terlebih dahulu.

Andy S
sumber
0

Salah satu cara untuk mengatasi kesalahan ini adalah dengan meletakkan ForeignKey atribut di atas properti yang Anda inginkan sebagai kunci asing dan menambahkan properti navigasi.

Catatan: Dalam ForeignKeyatribut, antara tanda kurung dan tanda kutip ganda, tempatkan nama kelas yang dimaksud dengan cara ini.

masukkan deskripsi gambar di sini

Oscar Echaverry
sumber
Harap tambahkan penjelasan minimal dalam jawaban itu sendiri, karena tautan yang disediakan mungkin tidak tersedia di masa mendatang.
n4m31ess_c0d3r
2
Itu harus nama properti navigasi , bukan kelas.
Harun