wake-up-neo.com

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

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 Verknüpfung mit flüssige API ? Oder gibt es eine bessere Möglichkeit, die Zuordnungstabelle zu erstellen?

277
hgdean

Es ist nicht möglich, eine Viele-zu-Viele-Beziehung mit einer angepassten Verknüpfungstabelle zu erstellen. In einer Viele-zu-Viele-Beziehung verwaltet EF die Join-Tabelle 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 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 die folgende 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 von Mitgliedern mit dem Namen "Smith" (wir nehmen an, dass es mehr als einen 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();

Sie können jetzt 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, die eine 99 in der Eigenschaft Something haben:

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

Wegen des faulen Ladens könnten die Dinge einfacher werden. Wenn Sie ein geladenes Member haben, sollten Sie in der Lage sein, die Kommentare ohne explizite Abfrage abzurufen:

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

Ich vermute, dass das langsame 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) Erstelle ein Mitglied und zwei Kommentare von diesem Mitglied:

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üge 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) Erstelle ein neues Mitglied und beziehe es auf den 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) Beziehung zwischen vorhandenem Mitglied2 und Kommentar3 erstellen:

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 in MemberComments gelöscht, da die Eins-zu-Viele-Beziehungen zwischen Member und MemberComments sowie zwischen Comment und MemberComments bestehen Setup mit kaskadierendem Löschen gemäß Konvention. Dies ist der Fall, weil MemberId und CommentId in MemberComment als Fremdschlüsseleigenschaften für die Navigationseigenschaften Member und Comment erkannt werden Da die FK-Eigenschaften vom Typ non-nullable int sind, ist die Beziehung erforderlich, die schließlich das Cascading-Delete-Setup verursacht. Ergibt in diesem Modell Sinn, denke ich.

502
Slauma

Ausgezeichnete Antwort von Slauma.

Ich poste einfach den Code, um dies zu tun, indem ich das flüssige API Mapping verwende.

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 von DbContext abgeleiteten Klasse können Sie dies 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 nein besser oder schlechter ist.

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

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

94
Esteban

@Esteban, der von dir bereitgestellte Code ist richtig, danke, aber unvollständig, ich habe es getestet. Es fehlen Eigenschaften in der Klasse "UserEmail":

    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 = "[email protected]" + 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
11
LeonardoX

Ich möchte eine Lösung vorschlagen, bei der beide Varianten einer Many-to-Many-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 EntitySet zugeordnet werden darf.

Diese Antwort ergänzt das bereits Gesagte und setzt keinen dieser Ansätze außer Kraft, 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);
     }
}

Von Saluma (@Saluma) Antwort

Wenn Sie jetzt zum Beispiel alle Kommentare von Mitgliedern mit LastName = "Smith" finden möchten, können Sie eine Abfrage wie die folgende 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 von Mitgliedern mit dem Namen "Smith" (wir nehmen an, dass es mehr als einen 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 die Kommentare eines Mitglieds mit Include() versehen möchten

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

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

1

TLDR; (in Verbindung 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 verknüpften Tabellen -> Speichern. edmx -> Generieren/Hinzufügen aus Datenbank -> Speichern.

Für diejenigen, die sich hier gefragt haben, wie eine Viele-zu-Viele-Beziehung mit Attributspalten hergestellt werden kann, die in der EF-EDMX-Datei angezeigt werden sollen (da sie derzeit nicht als Satz von Navigationseigenschaften angezeigt und behandelt werden würde), UND Sie haben diese Klassen generiert aus Ihrer Datenbanktabelle (oder Datenbank-First in MS-Jargon, glaube ich.)

Löschen Sie die beiden fraglichen Tabellen (für das OP-Beispiel Member und Comment) in Ihrer EDMX-Datei und fügen Sie sie erneut über "Modell aus Datenbank generieren" hinzu. (Versuchen Sie also nicht, Visual Studio sie aktualisieren zu lassen - löschen, speichern, hinzufügen, speichern)

Anschließend wird eine dritte Tabelle gemäß den hier vorgeschlagenen Angaben erstellt.

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, da dies der erste Link bei Google ist, der nach dem Problem sucht, aber zuerst von der DB-Seite kommt.

0
Andy S

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

Anmerkung: Platzieren Sie im Attribut ForeignKey zwischen Klammern und Anführungszeichen den Namen der Klasse, auf die auf diese Weise verwiesen wird.

enter image description here

0
Oscar Echaverry