Crea prima il codice, molti a molti, con campi aggiuntivi nella tabella delle associazioni


298

Ho questo scenario:

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

Come configuro la mia associazione con un'API fluente ? O c'è un modo migliore per creare la tabella delle associazioni?

Risposte:


524

Non è possibile creare una relazione molti-a-molti con una tabella di join personalizzata. In una relazione molti-a-molti EF gestisce la tabella di join internamente e nascosta. È una tabella senza una classe Entity nel tuo modello. Per lavorare con una tabella di join di questo tipo con proprietà aggiuntive, dovrai creare in realtà due relazioni uno-a-molte. Potrebbe apparire così:

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

Se ora desideri trovare tutti i commenti dei membri con LastName= "Smith", ad esempio, puoi scrivere una query come questa:

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

... o ...

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

Oppure per creare un elenco di membri con il nome "Smith" (supponiamo che ce ne sia più di uno) insieme ai loro commenti è possibile utilizzare una proiezione:

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

Se vuoi trovare tutti i commenti di un membro con MemberId= 1:

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

Ora puoi anche filtrare in base alle proprietà nella tabella dei join (che non sarebbe possibile in una relazione molti-a-molti), ad esempio: Filtra tutti i commenti del membro 1 che hanno una proprietà 99 Something:

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

A causa del caricamento lento, le cose potrebbero diventare più facili. Se hai caricato Member, dovresti essere in grado di ottenere i commenti senza una query esplicita:

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

Immagino che il caricamento lento recupererà automaticamente i commenti dietro le quinte.

modificare

Solo per divertimento qualche esempio in più su come aggiungere entità e relazioni e come eliminarle in questo modello:

1) Crea un membro e due commenti di questo membro:

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) Aggiungi un terzo commento di 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) Crea un nuovo membro e collegalo al commento2 esistente:

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) Creare una relazione tra membro2 esistente e commento3:

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) Cancella di nuovo questa relazione:

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) Elimina membro1 e tutte le sue relazioni con i commenti:

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

Questo elimina anche le relazioni MemberCommentsperché le relazioni uno-a-molti tra Membere MemberCommentse tra Commente MemberCommentssono impostate con eliminazione a cascata per convenzione. E questo è il caso perché MemberIde CommentIdin MemberCommentvengono rilevati come proprietà di chiave esterna per le proprietà di navigazione Membere Commente poiché le proprietà FK sono di tipo non nullable int, è necessaria una relazione che alla fine provoca il collegamento a cascata-cancellazione-installazione. Ha senso in questo modello, penso.


1
Grazie. Apprezzo molto le informazioni aggiuntive fornite.
hgdean,

7
@hgdean: ho spammato qualche altro esempio, scusate, ma è un modello interessante e ogni tanto ci sono domande su molti-a-molti con dati aggiuntivi nella tabella di join. Ora per la prossima volta ho qualcosa a cui collegarmi ... :)
Slauma,

4
@Esteban: non viene sovrascritto OnModelCreating. L'esempio si basa solo su convenzioni di mappatura e annotazioni di dati.
Slauma,

4
Nota: se usi questo approccio senza l'API Fluente assicurati di controllare nel tuo database che hai solo una chiave composita con MemberIde CommentIdcolonne e non una terza colonna aggiuntiva Member_CommentId(o qualcosa del genere) - il che significa che non avevi nomi esatti corrispondenti attraverso gli oggetti per le tue chiavi
Simon_Weaver,

3
@Simon_Weaver (o chiunque possa conoscere la risposta) Ho una situazione simile ma vorrei avere la chiave primaria "MemberCommentID" per quella tabella, è possibile o no? Attualmente sto ottenendo un'eccezione, si prega di dare un'occhiata alla mia domanda, ho davvero bisogno di aiuto ... stackoverflow.com/questions/26783934/...
duxfox--

97

Ottima risposta di Slauma.

Pubblicherò semplicemente il codice per farlo usando la mappatura API fluente .

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

Sulla tua DbContextclasse derivata potresti fare questo:

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

Ha lo stesso effetto della risposta accettata, con un approccio diverso, che non è migliore né peggiore.

EDIT: Ho cambiato CreatedDate da bool a DateTime.

EDIT 2: A causa della mancanza di tempo ho inserito un esempio da un'applicazione a cui sto lavorando per essere sicuro che funzioni.


1
Penso che sia sbagliato. Stai creando una relazione M: M qui dove deve essere 1: M per entrambe le entità.
CHS,

1
@CHS In your classes you can easily describe a many to many relationship with properties that point to each other.tratto da: msdn.microsoft.com/en-us/data/hh134698.aspx . Julie Lerman non può sbagliarsi.
Esteban,

1
Esteban, la mappatura delle relazioni è davvero errata. @CHS ha ragione su questo. Julie Lerman sta parlando di una "vera" relazione molti-a-molti, mentre qui abbiamo un esempio per un modello che non può essere mappato come molti-a-molti. Anche la tua mappatura non verrà compilata perché non hai una Commentsproprietà in Member. E non puoi semplicemente risolvere questo problema rinominando la HasManychiamata in MemberCommentsperché l' MemberCommententità non ha una raccolta inversa per WithMany. In effetti è necessario configurare due relazioni uno-a-molti per ottenere il mapping corretto.
Slauma,

2
Grazie. Ho seguito questa soluzione per fare il mapping molti-a-molti.
Thomas.Benz,

Non lo so, ma funziona meglio con MySql. Senza il builder, Mysql mi lancia un errore quando provo la migrazione.
Rodrigo Prieto,

11

@Esteban, il codice che hai fornito è giusto, grazie, ma incompleto, l'ho provato. Mancano proprietà nella classe "UserEmail":

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

Pubblico il codice che ho testato se qualcuno è interessato. Saluti

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

3

Voglio proporre una soluzione in cui sia possibile ottenere entrambi i gusti di una configurazione molti-a-molti.

Il "problema" è che dobbiamo creare una vista che abbia come target la tabella di join, poiché EF convalida che la tabella di uno schema possa essere mappata al massimo una volta per EntitySet.

Questa risposta aggiunge ciò che è già stato detto nelle risposte precedenti e non ignora nessuno di questi approcci, ma si basa su di essi.

Il modello:

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

La configurazione:

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

Il contesto:

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

Da (@Saluma) di Saluma risposta

Se ora vuoi trovare tutti i commenti dei membri con LastName = "Smith", ad esempio, puoi scrivere una query come questa:

Funziona ancora ...

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

... ma ora potrebbe anche essere ...

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

Oppure per creare un elenco di membri con il nome "Smith" (supponiamo che ce ne sia più di uno) insieme ai loro commenti è possibile utilizzare una proiezione:

Funziona ancora ...

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

... ma ora potrebbe anche essere ...

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

Se vuoi rimuovere un commento da un membro

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

member.Comments.Remove(comment);

Se vuoi Include()commenti di un membro

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

Tutto questo sembra zucchero sintattico, tuttavia ti offre alcuni vantaggi se sei disposto a passare attraverso la configurazione aggiuntiva. In entrambi i casi sembra che tu sia in grado di ottenere il meglio da entrambi gli approcci.


Apprezzo la maggiore leggibilità durante la digitazione delle query LINQ. Potrei semplicemente adottare questo metodo. Devo chiedere: EF EntitySet aggiorna automaticamente anche la vista nel database? Sei d'accordo sul fatto che questo sembra simile al [Sugar] come descritto nel piano EF5.0? github.com/dotnet/EntityFramework.Docs/blob/master/…
Krptodr

Mi chiedo perché sembri ridefinire EntityTypeConfiguration<EntityType>la chiave e le proprietà del tipo di entità. Ad esempio Property(x => x.MemberID).HasColumnType("int").IsRequired();sembra essere ridondante con public int MemberID { get; set; }. Potresti chiarire la mia comprensione confusa per favore?
Min

0

TLDR; (semi-correlato a un bug dell'editor EF in EF6 / VS2012U5) se si genera il modello dal DB e non è possibile visualizzare la tabella m: m attribuita: eliminare le due tabelle correlate -> Salva .edmx -> Genera / aggiungi dal database - > Salva.

Per coloro che sono venuti qui chiedendosi come ottenere una relazione molti-a-molti con le colonne degli attributi da mostrare nel file EF .edmx (poiché al momento non verrebbe mostrato e trattato come un insieme di proprietà di navigazione), E hai generato queste classi dalla tua tabella di database (o prima database in gergo MS, credo.)

Eliminare le 2 tabelle in questione (per prendere l'esempio OP, Member e Comment) nel proprio .edmx e aggiungerle nuovamente tramite "Genera modello dal database". (ovvero non tentare di consentire ad Visual Studio di aggiornarli - elimina, salva, aggiungi, salva)

Creerà quindi una terza tabella in linea con quanto suggerito qui.

Ciò è rilevante nei casi in cui inizialmente viene aggiunta una relazione molti-a-molti e gli attributi vengono progettati successivamente nel DB.

Questo non è stato immediatamente chiaro da questo thread / Google. Quindi basta metterlo lì in quanto questo è il link n. 1 su Google che cerca il problema, ma che proviene innanzitutto dal lato DB.


0

Un modo per risolvere questo errore è mettere l' ForeignKeyattributo in cima alla proprietà desiderata come chiave esterna e aggiungere la proprietà di navigazione.

Nota: ForeignKeynell'attributo, tra parentesi e virgolette, inserire il nome della classe a cui si fa riferimento in questo modo.

inserisci qui la descrizione dell'immagine


Aggiungi una spiegazione minima nella risposta stessa, poiché il link fornito potrebbe non essere disponibile in futuro.
n4m31ess_c0d3r

2
Dovrebbe essere il nome della proprietà di navigazione , piuttosto che la classe.
Aaron,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.