Erstellen Sie zuerst Code, viele bis viele, mit zusätzlichen Feldern in der Zuordnungstabelle

297

Ich habe dieses Szenario:

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

Wie konfiguriere ich meine Zuordnung zur fließenden API ? Oder gibt es eine bessere Möglichkeit, die Zuordnungstabelle zu erstellen?

hgdean
quelle

Antworten:

524

Es ist nicht möglich, eine Viele-zu-Viele-Beziehung mit einer benutzerdefinierten Verknüpfungstabelle zu erstellen. In einer Viele-zu-Viele-Beziehung verwaltet EF die Verknüpfungstabelle intern und versteckt. Es ist eine Tabelle ohne Entitätsklasse in Ihrem Modell. Um mit einer solchen Verknüpfungstabelle mit zusätzlichen Eigenschaften zu arbeiten, müssen Sie tatsächlich zwei Eins-zu-Viele-Beziehungen erstellen. Es könnte so aussehen:

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

Wenn Sie jetzt beispielsweise alle Kommentare von Mitgliedern mit LastName= "Smith" finden möchten, können Sie eine Abfrage wie folgt schreiben:

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

... oder ...

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

Oder um eine Liste der Mitglieder mit dem Namen "Smith" (wir nehmen an, dass es mehr als eines gibt) zusammen mit ihren Kommentaren zu erstellen, können Sie eine Projektion verwenden:

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

Wenn Sie alle Kommentare eines Mitglieds mit MemberId= 1 finden möchten :

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

Jetzt können Sie auch nach den Eigenschaften in Ihrer Join-Tabelle filtern (was in einer Viele-zu-Viele-Beziehung nicht möglich wäre). Beispiel: Filtern Sie alle Kommentare von Mitglied 1 mit einer 99-Eigenschaft Something:

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

Durch das verzögerte Laden könnten die Dinge einfacher werden. Wenn Sie eine geladen haben Member, sollten Sie in der Lage sein, die Kommentare ohne eine explizite Abfrage zu erhalten:

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

Ich denke, dass das verzögerte Laden die Kommentare automatisch hinter den Kulissen abruft.

Bearbeiten

Nur zum Spaß ein paar Beispiele mehr, wie man Entitäten und Beziehungen hinzufügt und wie man sie in diesem Modell löscht:

1) Erstellen Sie ein Mitglied und zwei Kommentare dieses Mitglieds:

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) Fügen Sie einen dritten Kommentar von member1 hinzu:

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) Erstellen Sie ein neues Mitglied und verknüpfen Sie es mit dem vorhandenen Kommentar2:

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) Erstellen Sie eine Beziehung zwischen vorhandenem Mitglied2 und Kommentar3:

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) Löschen Sie diese Beziehung erneut:

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) Löschen Sie member1 und alle seine Beziehungen zu den Kommentaren:

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

Dadurch werden auch die Beziehungen gelöscht, MemberCommentsda die Eins-zu-Viele-Beziehungen zwischen Memberund MemberCommentsund zwischen Commentund MemberCommentsgemäß Konvention mit kaskadierendem Löschen eingerichtet werden. Und dies ist der Fall, weil MemberIdund CommentIdin MemberCommentals Fremdschlüsseleigenschaften für die Memberund CommentNavigationseigenschaften erkannt werden und da die FK-Eigenschaften vom Typ nicht nullbar sind, ist intdie Beziehung erforderlich, die schließlich das Kaskadierungs-Lösch-Setup verursacht. Sinnvoll in diesem Modell, denke ich.

Slauma
quelle
1
Danke dir. Vielen Dank für die zusätzlichen Informationen, die Sie zur Verfügung gestellt haben.
hgdean
7
@hgdean: Ich habe noch ein paar Beispiele als Spam versendet, sorry, aber es ist ein interessantes Modell, und hier und da tauchen hin und wieder Fragen zu vielen zu vielen mit zusätzlichen Daten in der Join-Tabelle auf. Jetzt für das nächste Mal habe ich etwas zu verlinken ... :)
Slauma
4
@Esteban: Es gibt keine überschriebenen OnModelCreating. Das Beispiel basiert nur auf Zuordnungskonventionen und Datenanmerkungen.
Slauma
4
Hinweis: Wenn Sie diesen Ansatz ohne Fluent API verwenden, stellen Sie sicher, dass Sie in Ihrer Datenbank überprüfen, ob Sie nur einen zusammengesetzten Schlüssel mit MemberIdund CommentIdSpalten und keine zusätzliche dritte Spalte Member_CommentId(oder ähnliches) haben. Dies bedeutet, dass Sie keine genau übereinstimmenden Namen hatten über Objekte für Ihre Schlüssel
Simon_Weaver
3
@Simon_Weaver (oder jeder, der die Antwort kennt) Ich habe eine ähnliche Situation, möchte aber den Primärschlüssel "MemberCommentID" für diese Tabelle haben. Ist dies möglich oder nicht? Ich bekomme derzeit eine Ausnahme, bitte werfen Sie einen Blick auf meine Frage, ich brauche wirklich Hilfe ... stackoverflow.com/questions/26783934/…
duxfox--
97

Hervorragende Antwort von Slauma.

Ich werde einfach den Code veröffentlichen, um dies mithilfe der fließenden API- Zuordnung zu tun .

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

In Ihrer DbContextabgeleiteten Klasse können Sie Folgendes tun:

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

Es hat den gleichen Effekt wie die akzeptierte Antwort, mit einem anderen Ansatz, der weder besser noch schlechter ist.

BEARBEITEN: Ich habe CreatedDate von bool in DateTime geändert.

EDIT 2: Aus Zeitgründen habe ich ein Beispiel aus einer Anwendung platziert, an der ich arbeite, um sicherzustellen, dass dies funktioniert.

Esteban
quelle
1
Ich denke das ist falsch. Sie erstellen hier eine M: M-Beziehung, in der es für beide Entitäten 1: M sein muss.
CHS
1
@CHS In your classes you can easily describe a many to many relationship with properties that point to each other.entnommen aus: msdn.microsoft.com/en-us/data/hh134698.aspx . Julie Lerman kann sich nicht irren.
Esteban
1
Esteban, die Beziehungszuordnung ist wirklich falsch. @CHS hat damit Recht. Julie Lerman spricht von einer "wahren" Viele-zu-Viele-Beziehung, während wir hier ein Beispiel für ein Modell haben, das nicht so viele-zu-viele abgebildet werden kann. Ihr Mapping wird nicht einmal kompiliert, da Sie keine CommentsEigenschaft in haben Member. Und Sie können dies nicht einfach beheben, indem Sie den HasManyAufruf in umbenennen , MemberCommentsda die MemberCommentEntität keine inverse Sammlung für hat WithMany. Tatsächlich müssen Sie zwei Eins-zu-Viele-Beziehungen konfigurieren , um die richtige Zuordnung zu erhalten.
Slauma
2
Danke dir. Ich folgte dieser Lösung, um das Mapping von vielen zu vielen durchzuführen.
Thomas.Benz
Ich weiß es nicht, aber das funktioniert besser mit MySql. Ohne den Builder hat MySQL mir beim Versuch der Migration einen Fehler gemeldet.
Rodrigo Prieto
11

@Esteban, der von Ihnen angegebene Code ist richtig, danke, aber unvollständig, ich habe ihn getestet. In der Klasse "UserEmail" fehlen Eigenschaften:

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

Ich poste den Code, den ich getestet habe, wenn jemand interessiert ist. Grüße

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
quelle
3

Ich möchte eine Lösung vorschlagen, bei der beide Varianten einer Viele-zu-Viele-Konfiguration erreicht werden können.

Der "Haken" ist, dass wir eine Ansicht erstellen müssen, die auf die Join-Tabelle abzielt, da EF überprüft, ob die Tabelle eines Schemas höchstens einmal pro Tabelle zugeordnet werden darf EntitySet.

Diese Antwort ergänzt das, was bereits in früheren Antworten gesagt wurde, und überschreibt keinen dieser Ansätze, sondern baut auf ihnen auf.

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

Die Konfiguration:

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

Der Kontext:

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

Aus Salumas (@Saluma) Antwort

Wenn Sie jetzt beispielsweise alle Kommentare von Mitgliedern mit LastName = "Smith" finden möchten, können Sie eine Abfrage wie folgt schreiben:

Das funktioniert immer noch ...

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

... könnte aber jetzt auch sein ...

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

Oder um eine Liste der Mitglieder mit dem Namen "Smith" (wir nehmen an, dass es mehr als eines gibt) zusammen mit ihren Kommentaren zu erstellen, können Sie eine Projektion verwenden:

Das funktioniert immer noch ...

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

... könnte aber jetzt auch sein ...

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

Wenn Sie einen Kommentar von einem Mitglied entfernen möchten

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

member.Comments.Remove(comment);

Wenn Sie Include()die Kommentare eines Mitglieds möchten

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

Dies alles fühlt sich wie syntaktischer Zucker an, bringt Ihnen jedoch einige Vorteile, wenn Sie bereit sind, die zusätzliche Konfiguration durchzugehen. In beiden Fällen scheinen Sie in der Lage zu sein, das Beste aus beiden Ansätzen herauszuholen.

Mauricio Morales
quelle
Ich schätze die verbesserte Lesbarkeit beim Eingeben der LINQ-Abfragen. Möglicherweise muss ich nur diese Methode anwenden. Ich muss fragen, ob das EF EntitySet auch die Ansicht in der Datenbank automatisch aktualisiert. Würden Sie zustimmen, dass dies dem im EF5.0-Plan beschriebenen [Zucker] ähnlich scheint? github.com/dotnet/EntityFramework.Docs/blob/master/…
Krptodr
Ich frage mich, warum Sie sich neu zu definieren scheinen EntityTypeConfiguration<EntityType> den Schlüssel und die Eigenschaften des Entitätstyps . ZB Property(x => x.MemberID).HasColumnType("int").IsRequired();scheint überflüssig zu sein public int MemberID { get; set; }. Könnten Sie bitte mein verwirrendes Verständnis klären?
Minuten
0

TLDR; (Teilweise verwandt mit einem EF-Editor-Fehler in EF6 / VS2012U5) Wenn Sie das Modell aus der Datenbank generieren und die zugeordnete m: m-Tabelle nicht sehen können: Löschen Sie die beiden zugehörigen Tabellen -> .edmx speichern -> Aus Datenbank generieren / hinzufügen - > Speichern.

Für diejenigen, die hierher gekommen sind und sich gefragt haben, wie eine Viele-zu-Viele-Beziehung zu Attributspalten in der EF .edmx-Datei angezeigt werden kann (da diese derzeit nicht angezeigt und als Satz von Navigationseigenschaften behandelt wird), UND Sie haben diese Klassen generiert aus Ihrer Datenbanktabelle (oder Datenbank zuerst in MS-Jargon, glaube ich.)

Löschen Sie die beiden fraglichen Tabellen (um das OP-Beispiel "Mitglied" und "Kommentar" zu verwenden) in Ihrer .edmx-Datei und fügen Sie sie erneut über "Modell aus Datenbank generieren" hinzu. (dh versuchen Sie nicht, Visual Studio sie aktualisieren zu lassen - löschen, speichern, hinzufügen, speichern)

Anschließend wird eine dritte Tabelle erstellt, die den hier vorgeschlagenen entspricht.

Dies ist in Fällen relevant, in denen zunächst eine reine Viele-zu-Viele-Beziehung hinzugefügt wird und die Attribute später in der Datenbank entworfen werden.

Dies war aus diesem Thread / Googeln nicht sofort ersichtlich. Stellen Sie es einfach da raus, da dies Link Nr. 1 bei Google ist, der nach dem Problem sucht, aber zuerst von der DB-Seite kommt.

Andy S.
quelle
0

Eine Möglichkeit, diesen Fehler zu beheben, besteht darin, das zu setzen ForeignKey Attribut als Fremdschlüssel über die gewünschte Eigenschaft und die Navigationseigenschaft hinzuzufügen.

Hinweis: ForeignKeyPlatzieren Sie im Attribut zwischen Klammern und doppelten Anführungszeichen den Namen der Klasse, auf die auf diese Weise verwiesen wird.

Geben Sie hier die Bildbeschreibung ein

Oscar Echaverry
quelle
Bitte fügen Sie der Antwort selbst eine minimale Erklärung hinzu, da der angegebene Link in Zukunft möglicherweise nicht mehr verfügbar ist.
n4m31ess_c0d3r
2
Es sollte der Name der Navigationseigenschaft und nicht die Klasse sein.
Aaron