Créez d'abord le code, plusieurs à plusieurs, avec des champs supplémentaires dans la table d'association

298

J'ai ce scénario:

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

Comment configurer mon association avec une API fluide ? Ou existe-t-il une meilleure façon de créer la table d'association?

hgdean
la source

Réponses:

524

Il n'est pas possible de créer une relation plusieurs-à-plusieurs avec une table de jointure personnalisée. Dans une relation plusieurs-à-plusieurs, EF gère la table de jointure en interne et est masquée. Il s'agit d'une table sans classe Entity dans votre modèle. Pour travailler avec une telle table de jointure avec des propriétés supplémentaires, vous devrez créer en fait deux relations un-à-plusieurs. Cela pourrait ressembler à ceci:

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

Si vous voulez maintenant trouver tous les commentaires des membres avec LastName= "Smith" par exemple, vous pouvez écrire une requête comme celle-ci:

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

... ou ...

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

Ou pour créer une liste de membres avec le nom "Smith" (nous supposons qu'il y en a plus d'un) avec leurs commentaires, vous pouvez utiliser une projection:

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

Si vous souhaitez retrouver tous les commentaires d'un membre avec MemberId= 1:

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

Maintenant, vous pouvez également filtrer par les propriétés de votre table de jointure (ce qui ne serait pas possible dans une relation plusieurs-à-plusieurs), par exemple: Filtrez tous les commentaires du membre 1 qui ont une propriété 99 dans Something:

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

En raison du chargement paresseux, les choses pourraient devenir plus faciles. Si vous avez un chargé, Membervous devriez pouvoir obtenir les commentaires sans requête explicite:

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

Je suppose que le chargement paresseux récupérera automatiquement les commentaires dans les coulisses.

Éditer

Juste pour le plaisir, quelques exemples supplémentaires sur la façon d'ajouter des entités et des relations et de les supprimer dans ce modèle:

1) Créez un membre et deux commentaires de ce membre:

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) Ajoutez un troisième commentaire de membre1:

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) Créez un nouveau membre et associez-le au commentaire existant2:

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) Créez une relation entre le membre2 existant et le commentaire3:

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) Supprimez à nouveau cette relation:

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) Supprimez le membre1 et toutes ses relations avec les commentaires:

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

Cela supprime également les relations MemberCommentscar les relations un à plusieurs entre Memberet MemberCommentset entre Commentet MemberCommentssont configurées avec la suppression en cascade par convention. Et cela est le cas parce que MemberIdet CommentIden MemberCommentsont détectés comme des propriétés clés étrangères pour les Memberet Commentpropriétés navigation et étant donné que les propriétés FK sont de type non annulable intla relation est nécessaire , ce qui provoque finalement la configuration en cascade-suppression. Cela a du sens dans ce modèle, je pense.

Slauma
la source
1
Je vous remercie. J'apprécie beaucoup les informations supplémentaires que vous avez fournies.
hgdean
7
@hgdean: J'ai spammé quelques autres exemples, désolé, mais c'est un modèle intéressant et des questions sur plusieurs à plusieurs avec des données supplémentaires dans la table de jointure se posent de temps en temps ici. Maintenant, pour la prochaine fois, j'ai quelque chose à lier à ... :)
Slauma
4
@Esteban: Il n'y a pas de substitution OnModelCreating. L'exemple repose uniquement sur les conventions de mappage et les annotations de données.
Slauma
4
Remarque: si vous utilisez cette approche sans faire API Fluent que vous vérifiez dans votre base de données que vous avez seulement une clé composite MemberIdet CommentIdcolonnes et non une troisième colonne supplémentaire Member_CommentId(ou quelque chose comme ça) - ce qui signifie que vous n'avez pas des noms de correspondance exacte à travers des objets pour vos clés
Simon_Weaver
3
@Simon_Weaver (ou toute personne pouvant connaître la réponse) J'ai une situation similaire mais je voudrais avoir la clé primaire "MemberCommentID" pour cette table, est-ce possible ou non? Je reçois actuellement une exception, veuillez jeter un œil à ma question, j'ai vraiment besoin d'aide ... stackoverflow.com/questions/26783934/…
duxfox--
98

Excellente réponse de Slauma.

Je vais juste publier le code pour ce faire en utilisant le mappage d' API fluide .

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

Sur votre DbContextclasse dérivée, vous pouvez faire ceci:

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

Elle a le même effet que la réponse acceptée, avec une approche différente, qui n'est ni meilleure ni pire.

EDIT: j'ai changé CreatedDate de bool à DateTime.

EDIT 2: Par manque de temps, j'ai placé un exemple à partir d'une application sur laquelle je travaille pour être sûr que cela fonctionne.

Esteban
la source
1
Je pense que cela est faux. Vous créez une relation M: M ici où elle doit être 1: M pour les deux entités.
CHS
1
@CHS In your classes you can easily describe a many to many relationship with properties that point to each other.extrait de: msdn.microsoft.com/en-us/data/hh134698.aspx . Julie Lerman ne peut pas se tromper.
Esteban
1
Esteban, le mappage des relations est vraiment incorrect. @CHS a raison à ce sujet. Julie Lerman parle d'une "vraie" relation plusieurs-à-plusieurs alors que nous avons ici un exemple de modèle qui ne peut pas être mappé en plusieurs-à-plusieurs. Votre mappage ne sera même pas compilé car vous n'avez pas de Commentspropriété dans Member. Et vous ne pouvez pas simplement résoudre ce problème en renommant l' HasManyappel MemberCommentscar l' MemberCommententité n'a pas de collection inverse pour WithMany. En fait, vous devez configurer deux relations un-à-plusieurs pour obtenir le bon mappage.
Slauma
2
Je vous remercie. J'ai suivi cette solution pour faire le mappage plusieurs-à-plusieurs.
Thomas.Benz
Je ne sais pas, mais cela fonctionne mieux avec MySql. Sans le générateur, Mysql m'a renvoyé une erreur lorsque j'ai essayé la migration.
Rodrigo Prieto
11

@Esteban, le code que vous avez fourni est correct, merci, mais incomplet, je l'ai testé. Il manque des propriétés dans la classe "UserEmail":

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

Je poste le code que j'ai testé si quelqu'un est intéressé. Cordialement

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
la source
3

Je veux proposer une solution où les deux saveurs d'une configuration plusieurs-à-plusieurs peuvent être obtenues.

Le «catch» est que nous devons créer une vue qui cible la table de jointure, car EF valide que la table d'un schéma peut être mappée au plus une fois par EntitySet.

Cette réponse s'ajoute à ce qui a déjà été dit dans les réponses précédentes et ne remplace aucune de ces approches, elle s'appuie sur elles.

Le modèle:

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 configuration:

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

Le contexte:

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

De la réponse de Saluma (@Saluma)

Si vous voulez maintenant trouver tous les commentaires des membres avec LastName = "Smith" par exemple, vous pouvez écrire une requête comme celle-ci:

Cela fonctionne toujours ...

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

... mais pourrait aussi l'être maintenant ...

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

Ou pour créer une liste de membres avec le nom "Smith" (nous supposons qu'il y en a plus d'un) avec leurs commentaires, vous pouvez utiliser une projection:

Cela fonctionne toujours ...

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

... mais pourrait aussi l'être maintenant ...

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

Si vous souhaitez supprimer un commentaire d'un membre

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

member.Comments.Remove(comment);

Si vous voulez Include()les commentaires d'un membre

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

Tout cela ressemble à du sucre syntaxique, mais cela vous donne quelques avantages si vous êtes prêt à passer par la configuration supplémentaire. Quoi qu'il en soit, vous semblez être en mesure de tirer le meilleur parti des deux approches.

Mauricio Morales
la source
J'apprécie la lisibilité accrue lors de la saisie des requêtes LINQ. Il se peut que je doive simplement adopter cette méthode. Je dois demander, l'EF EntitySet met-il également automatiquement à jour la vue dans la base de données? Seriez-vous d'accord pour dire que cela semble similaire au [Sugar] tel que décrit dans le plan EF5.0? github.com/dotnet/EntityFramework.Docs/blob/master/…
Krptodr
Je me demande pourquoi vous semblez redéfinir EntityTypeConfiguration<EntityType>la clé et les propriétés du type d'entité. Par exemple, Property(x => x.MemberID).HasColumnType("int").IsRequired();semble être redondant avec public int MemberID { get; set; }. Pourriez-vous effacer ma compréhension confuse s'il vous plaît?
20
0

TLDR; (semi-lié à un bogue de l'éditeur EF dans EF6 / VS2012U5) si vous générez le modèle à partir de la base de données et que vous ne pouvez pas voir la table m: m attribuée: supprimez les deux tables liées -> Enregistrer .edmx -> Générer / ajouter à partir de la base de données - > Enregistrer.

Pour ceux qui sont venus ici se demander comment obtenir une relation plusieurs-à-plusieurs avec des colonnes d'attributs à afficher dans le fichier EF .edmx (car il ne serait actuellement pas affiché et traité comme un ensemble de propriétés de navigation), ET vous avez généré ces classes à partir de votre table de base de données (ou de la base de données d'abord dans le jargon MS, je crois.)

Supprimez les 2 tables en question (pour prendre l'exemple OP, Member et Comment) dans votre .edmx et ajoutez-les à nouveau via 'Générer le modèle à partir de la base de données'. (c.-à-d. n'essayez pas de laisser Visual Studio les mettre à jour - supprimer, enregistrer, ajouter, enregistrer)

Il créera ensuite un 3e tableau en ligne avec ce qui est suggéré ici.

Ceci est pertinent dans les cas où une relation plusieurs-à-plusieurs pure est ajoutée dans un premier temps, et les attributs sont conçus dans la base de données plus tard.

Ce n'était pas immédiatement clair à partir de ce fil / Google. Il suffit donc de le mettre en ligne car il s'agit du lien n ° 1 sur Google à la recherche du problème, mais venant du côté DB en premier.

Andy S
la source
0

Une façon de résoudre cette erreur consiste à placer l' ForeignKeyattribut au-dessus de la propriété que vous souhaitez en tant que clé étrangère et à ajouter la propriété de navigation.

Remarque: Dans l' ForeignKeyattribut, entre parenthèses et guillemets doubles, placez le nom de la classe référencée de cette manière.

entrez la description de l'image ici

Oscar Echaverry
la source
Veuillez ajouter une explication minimale dans la réponse elle-même, car le lien fourni risque de ne plus être disponible à l'avenir.
n4m31ess_c0d3r
2
Il doit s'agir du nom de la propriété de navigation plutôt que de la classe.
aaron