Najpierw utwórz kod, wiele do wielu, z dodatkowymi polami w tabeli asocjacji

297

Mam ten scenariusz:

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

Jak skonfigurować powiązanie z płynnym interfejsem API ? Czy jest lepszy sposób na utworzenie tabeli asocjacji?

hgdean
źródło

Odpowiedzi:

524

Nie można utworzyć relacji wiele do wielu za pomocą dostosowanej tabeli łączenia. W relacji wiele do wielu EF zarządza wewnętrzną i ukrytą tabelą łączenia. To stół bez klasy Entity w twoim modelu. Aby pracować z taką tabelą łączenia z dodatkowymi właściwościami, musisz utworzyć dwie relacje jeden do wielu. Może to wyglądać tak:

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

Jeśli chcesz teraz znaleźć wszystkie komentarze członków z LastName= = Smith, możesz na przykład napisać takie zapytanie:

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

... lub ...

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

Lub, aby utworzyć listę członków o nazwie „Smith” (zakładamy, że jest ich więcej) wraz z ich komentarzami, możesz użyć projekcji:

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

Jeśli chcesz znaleźć wszystkie komentarze członka z MemberId= 1:

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

Teraz możesz również filtrować według właściwości w tabeli łączenia (co nie byłoby możliwe w relacji wiele do wielu), na przykład: Filtruj wszystkie komentarze członka 1, które mają właściwość 99 Something:

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

Z powodu leniwego ładowania rzeczy mogą stać się łatwiejsze. Jeśli masz załadowany Member, powinieneś być w stanie uzyskać komentarze bez wyraźnego zapytania:

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

Myślę, że leniwe ładowanie automatycznie pobierze komentarze za kulisami.

Edytować

Dla zabawy kilka przykładów, jak dodawać encje i relacje oraz jak je usuwać w tym modelu:

1) Utwórz jednego członka i dwa komentarze tego członka:

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) Dodaj trzeci komentarz członka1:

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) Utwórz nowego członka i powiąż go z istniejącym komentarzem2:

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) Utwórz relację między istniejącym członkiem2 a komentarzem3:

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) Usuń tę relację ponownie:

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) Usuń członka 1 i wszystkie jego powiązania z komentarzami:

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

Spowoduje to usunięcie relacji w MemberCommentszbyt ponieważ jeden-do-wielu relacje między Memberi MemberCommentsoraz między Commenti MemberCommentssą ustawione z kaskadowego usuwania umownie. I to jest tak dlatego, że MemberIdi CommentIdw MemberCommentsą wykrywane jako obcych dla kluczowych właściwości Memberi Commentnawigacyjnych właściwości i od właściwości FK są typu non-pustych intwymagana jest relacja, która ostatecznie powoduje kaskadowe-delete-setup. Myślę, że ma sens w tym modelu.

Slauma
źródło
1
Dziękuję Ci. Bardzo doceniamy dodatkowe informacje, które podałeś.
hgdean
7
@hgdean: Przepraszam jeszcze kilka przykładów, przepraszam, ale to ciekawy model, a pytania o wiele do wielu z dodatkowymi danymi w tabeli łączenia pojawiają się od czasu do czasu. Teraz następnym razem mam coś do linku do ... :)
Slauma
4
@Esteban: Nie ma przesłonięcia OnModelCreating. Przykład opiera się tylko na konwencjach mapowania i adnotacjach danych.
Slauma,
4
Uwaga: jeśli używasz tego podejścia bez Fluent API Należy sprawdzić w bazie danych, że masz tylko klucz kompozytu MemberIdi CommentIdkolumn, a nie dodatkową trzecią kolumnę Member_CommentId(lub coś podobnego) - co oznacza, że nie mają dokładnych nazw dopasowania między obiektami dla twoich kluczy
Simon_Weaver
3
@ Simon_Weaver (lub każdy, kto może znać odpowiedź) Mam podobną sytuację, ale chciałbym mieć klucz podstawowy „MemberCommentID” dla tej tabeli, czy jest to możliwe, czy nie? Obecnie otrzymuję wyjątek, proszę spojrzeć na moje pytanie, naprawdę potrzebuję pomocy ... stackoverflow.com/questions/26783934/…
duxfox--
97

Doskonała odpowiedź Slauma.

Po prostu opublikuję kod, aby to zrobić, korzystając z płynnego mapowania interfejsu 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; }
}

W DbContextklasie pochodnej możesz to zrobić:

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

Ma taki sam efekt jak zaakceptowana odpowiedź, z innym podejściem, co nie jest ani lepsze, ani gorsze.

EDYCJA: Zmieniłem CreatedDate z bool na DateTime.

EDYCJA 2: Z powodu braku czasu umieściłem przykład z aplikacji, nad którą pracuję, aby mieć pewność, że to zadziała.

Esteban
źródło
1
Myślę, że to źle. Tworzysz relację M: M tutaj, gdzie musi ona wynosić 1: M dla obu jednostek.
CHS
1
@CHS In your classes you can easily describe a many to many relationship with properties that point to each other.pochodzi z: msdn.microsoft.com/en-us/data/hh134698.aspx . Julie Lerman nie może się mylić.
Esteban
1
Esteban, mapowanie relacji jest naprawdę niepoprawne. @CHS ma rację w tej sprawie. Julie Lerman mówi o „prawdziwej” relacji wiele do wielu, a my mamy tutaj przykład modelu, którego nie można zmapować jako wiele do wielu. Twoje mapowanie nawet się nie skompiluje, ponieważ nie masz Commentswłaściwości Member. Nie można tego po prostu zmienić, zmieniając nazwę HasManywywołania, MemberCommentsponieważ MemberCommentjednostka nie ma kolekcji odwrotnej WithMany. W rzeczywistości musisz skonfigurować dwie relacje jeden do wielu, aby uzyskać odpowiednie mapowanie.
Slauma,
2
Dziękuję Ci. Zastosowałem to rozwiązanie, aby wykonać mapowanie wiele do wielu.
Thomas.Benz
Nie wiem, ale to działa lepiej z MySql. Bez konstruktora Mysql wyświetla błąd podczas próby migracji.
Rodrigo Prieto
11

@ Esteban, podany kod jest poprawny, dziękuję, ale niekompletny, przetestowałem go. Brakuje właściwości w klasie „UserEmail”:

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

Wysyłam kod, który przetestowałem, jeśli ktoś jest zainteresowany. pozdrowienia

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
źródło
3

Chcę zaproponować rozwiązanie, w którym można uzyskać oba smaki konfiguracji „wiele do wielu”.

„Złap” polega na tym, że musimy utworzyć widok ukierunkowany na tabelę łączenia, ponieważ EF sprawdza, czy tabela schematu może być mapowana co najwyżej raz na EntitySet.

Ta odpowiedź uzupełnia to, co zostało powiedziane w poprzednich odpowiedziach i nie zastępuje żadnego z tych podejść, opiera się na nich.

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

Konfiguracja:

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

Kontekst:

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

Od (@Saluma) Saluma za odpowiedź

Jeśli chcesz teraz znaleźć wszystkie komentarze członków o nazwie LastName = "Smith", możesz na przykład napisać takie zapytanie:

To nadal działa ...

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

... ale teraz może być również ...

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

Lub, aby utworzyć listę członków o nazwie „Smith” (zakładamy, że jest ich więcej) wraz z ich komentarzami, możesz użyć projekcji:

To nadal działa ...

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

... ale teraz może być również ...

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

Jeśli chcesz usunąć komentarz członka

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

member.Comments.Remove(comment);

Jeśli chcesz Include()dodać komentarz członka

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

Wszystko to przypomina cukier syntaktyczny, jednak daje ci kilka korzyści, jeśli chcesz przejść przez dodatkową konfigurację. Tak czy inaczej wydaje się, że jesteś w stanie uzyskać najlepsze z obu podejść.

Mauricio Morales
źródło
Doceniam zwiększoną czytelność podczas pisania zapytań LINQ. Być może będę musiał po prostu zastosować tę metodę. Muszę zapytać, czy EF EntitySet również automatycznie aktualizuje widok w bazie danych? Czy zgadzasz się, że wydaje się to podobne do [cukru] opisanego w planie EF5.0? github.com/dotnet/EntityFramework.Docs/blob/master/…
Krptodr
Zastanawiam się, dlaczego wydajesz się zmieniać definicję EntityTypeConfiguration<EntityType>klucza i właściwości typu encji. Np. Property(x => x.MemberID).HasColumnType("int").IsRequired();Wydaje się zbędny public int MemberID { get; set; }. Czy możesz wyjaśnić moje mylące zrozumienie, proszę?
min
0

TLDR; (częściowo związany z błędem edytora EF w EF6 / VS2012U5), jeśli generujesz model z DB i nie widzisz przypisanej tabeli m: m: Usuń dwie powiązane tabele -> Zapisz .edmx -> Wygeneruj / dodaj z bazy danych - > Zapisz.

Dla tych, którzy tu przybyli, zastanawiając się, jak uzyskać relację wiele do wielu z kolumnami atrybutów, aby wyświetlać się w pliku EF .edmx (ponieważ obecnie nie byłby wyświetlany i traktowany jako zestaw właściwości nawigacyjnych), ORAZ wygenerowałeś te klasy ze stołu bazy danych (lub, jak sądzę, pierwsza baza danych w MS Lingo).

Usuń 2 tabele, o których mowa (na przykład OP, Członek i Komentarz) w .edmx i dodaj je ponownie poprzez „Generuj model z bazy danych”. (tj. nie próbuj pozwolić Visual Studio na ich aktualizację - usuń, zapisz, dodaj, zapisz)

Następnie utworzy 3. tabelę zgodnie z tym, co jest tutaj sugerowane.

Jest to istotne w przypadkach, gdy najpierw dodaje się czystą relację wiele do wielu, a atrybuty są projektowane w DB później.

Nie było to od razu jasne z tego wątku / Googling. Więc po prostu go opublikuj, ponieważ jest to link nr 1 w Google, który szuka problemu, ale najpierw pochodzi od strony DB.

Andy S.
źródło
0

Jednym ze sposobów rozwiązania tego błędu jest umieszczenie ForeignKeyatrybutu nad właściwością, którą chcesz jako klucz obcy, i dodanie właściwości nawigacji.

Uwaga: W ForeignKeyatrybucie, pomiędzy nawiasami i podwójnymi cudzysłowami, umieść nazwę klasy, o której mowa w ten sposób.

wprowadź opis zdjęcia tutaj

Oscar Echaverry
źródło
Proszę dodać minimalne wyjaśnienie w samej odpowiedzi, ponieważ podany link może stać się niedostępny w przyszłości.
n4m31ess_c0d3r
2
Powinna to być nazwa właściwości nawigacji , a nie klasa.
aaron