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

Mam taki 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ć skojarzenie z fluent API ? A może istnieje lepszy sposób na utworzenie tabeli asocjacji?

Author: Peter Mortensen, 2011-08-13

6 answers

Nie można utworzyć relacji wiele do wielu za pomocą niestandardowej tabeli łączenia. W relacji wiele-do-wielu EF zarządza wewnętrznie tabelą join i jest ukryta. To tabela bez klasy Bytów w twoim modelu. Aby pracować z taką tabelą join z dodatkowymi właściwościami, będziesz musiał 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 teraz chcesz znaleźć wszystkie komentarze członków z LastName = "Smith" na przykład możesz napisać zapytanie tak:

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

...albo...

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

Lub utworzyć listę członków o nazwie " Smith "(Zakładamy, że jest więcej niż jeden) wraz z ich komentarzami można 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 użytkownika 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 join (co nie byłoby możliwe w relacji many-to-many), na przykład: Filtruj wszystkie komentarze członka 1, który ma 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 pytania explicite:

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

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

Edit

Dla zabawy kilka przykładów więcej jak dodawać byty i relacje oraz jak je usuwać w tym modelu:

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

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 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) Utwórz nowego członka i powiązaj go z istniejącym komentarzem 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) Utwórz relację między istniejącym członkiem 2 i komentarzem 3:

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ń ten związek jeszcze raz:

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ń member1 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();
}

To usuwa relacje w MemberComments również dlatego, że relacje jeden do wielu między Member i MemberComments i pomiędzy Comment i {[17] } są ustawione z kaskadowym usuwaniem według konwencji. Dzieje się tak, ponieważ MemberId i CommentId W MemberComment są wykrywane jako właściwości klucza obcego dla właściwości nawigacji Member i Comment, a ponieważ właściwości FK są typu non-nullable int, wymagana jest relacja, która ostatecznie powoduje ustawienie cascading-delete-setup. Myślę, że to ma sens w tym modelu.

 472
Author: Slauma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-14 13:05:35

Doskonała odpowiedź przez Slauma.

po prostu wyślę kod, aby to zrobić, używając mapowania fluent 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; }
}

Na Twojej DbContext klasie 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 przyjęta odpowiedź, z innym podejściem, które jest nie lepsze lub gorsze.

Edytuj: zmieniłem CreatedDate z bool na DateTime.

Edycja 2: Ze względu na brak czasu umieściłem przykład z aplikacji Pracuję nad tym, żeby to zadziałało.

 87
Author: Esteban,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-07 11:17:37

@Esteban, podany przez Ciebie kod jest prawidłowy, dzięki, ale niekompletny, Przetestowałem go. Brak właściwości w klasie "UserEmail":

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

Zamieszczam kod, który przetestowałem, jeśli ktoś jest zainteresowany. Regards

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
 10
Author: LeonardoX,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-03 10:04:11

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

Dla tych, którzy przybyli tutaj zastanawiając się, jak uzyskać wiele do wielu relacji z kolumnami atrybutów, aby pokazać w EF .plik edmx (gdyż obecnie nie pokazywałby się i był traktowany jako zbiór właściwości nawigacyjnych), a te klasy wygenerowałeś z tabela bazy danych (lub baza danych-pierwsza w MS lingo, jak sądzę.)

Usuń 2 tabele (aby wziąć przykład OP, Member i komentarz) w Twoim .edmx i dodać je ponownie poprzez 'Generuj model z bazy danych'. (tzn. nie próbuj pozwolić, aby Visual Studio je aktualizowało-Usuń , Zapisz, dodaj, Zapisz)

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

Jest to istotne w przypadkach, gdy na początku dodawana jest czysta relacja wiele do wielu, a atrybuty są zaprojektowany później w DB.

To nie było od razu jasne z tego wątku / googlowania. Więc po prostu umieszczając to tam, ponieważ jest to link #1 w Google, szukający problemu, ale pochodzący od strony DB.

 0
Author: Andy S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-03 10:27:53

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

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

Tutaj wpisz opis obrazka

 0
Author: Oscar Echaverry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-29 18:08:34

Chcę zaproponować rozwiązanie, w którym można osiągnąć oba smaki konfiguracji wielu do wielu.

"catch" polega na tym, że musimy stworzyć widok, który będzie skierowany na tabelę Join, ponieważ EF potwierdza, że tabela schematu może być mapowana co najwyżej raz na EntitySet.

Ta odpowiedź dodaje do tego, co już zostało powiedziane w poprzednich odpowiedziach i nie nadpisuje ż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; }
}

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

From Saluma ' s (@Saluma) answer

Jeśli chcesz teraz znaleźć wszystkie komentarze użytkowników z lastname = "Smith" na przykład możesz napisać zapytanie w następujący sposób:

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

...ale teraz też może być...

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

Lub stworzyć listę członków o nazwie "Smith" (Zakładamy, że jest więcej niż jeden) wraz z ich komentarzami można użyć rzut:

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 też może być...

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

Jeśli chcesz usunąć komentarz od członka

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

member.Comments.Remove(comment);

If you want to Include() a member ' s comments

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

To wszystko wygląda jak cukier składniowy, jednak daje Ci kilka korzyści, jeśli chcesz przejść przez dodatkową konfigurację. Tak czy inaczej wydaje się być w stanie uzyskać najlepsze z obu podejść.

 0
Author: Mauricio Morales,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-19 17:26:27