web-dev-qa-db-fra.com

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

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 API courante ? Ou existe-t-il un meilleur moyen de créer la table d'association?

277
hgdean

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. C'est une table sans classe d'entité dans votre modèle. Pour utiliser une telle table de jointure avec des propriétés supplémentaires, vous devrez créer 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), ainsi que 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 voulez trouver tous les commentaires d'un membre avec MemberId = 1:

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

Vous pouvez maintenant également filtrer 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 dont la propriété 99 est dans la propriété 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 Member chargé, vous devriez pouvoir obtenir les commentaires sans interrogation explicite:

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

Je suppose que le chargement paresseux récupérera automatiquement les commentaires en coulisse.

Éditer

Juste pour le plaisir, quelques exemples plus comment ajouter des entités et des relations et comment 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éer une relation entre membre2 existant et 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) Supprimer à 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) Supprimer le membre 1 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 dans MemberComments, car les relations un-à-plusieurs entre Member et MemberComments et entre Comment et MemberComments sont configurées avec une suppression en cascade par convention. Et c’est le cas parce que MemberId et CommentId dans MemberComment sont détectés comme propriétés de clé étrangère pour les propriétés de navigation Member et Comment et depuis que les propriétés FK sont de type non-nullable int la relation est requise, ce qui provoque finalement l'installation en cascade. Cela a du sens dans ce modèle, je pense.

502
Slauma

Excellent réponse de Slauma.

Je posterai simplement le code pour le faire en utilisant le mappage fluide 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; }
}

Vous pouvez faire ceci sur votre classe dérivée DbContext:

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

Cela a le même effet que la réponse acceptée, avec une approche différente, qui est no ni meilleur ni pire.

MODIFIER: J'ai changé CreatedDate de bool à DateTime.

EDIT 2: Par manque de temps, j'ai mis en exemple une application sur laquelle je travaille pour être sûr que cela fonctionne.

94
Esteban

@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
11
LeonardoX

Je souhaite proposer une solution permettant de réaliser les deux types de configuration plusieurs à plusieurs.

La "capture" 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 les construit.

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 Saluma (@Saluma) réponse

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 maintenant être aussi ...

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), ainsi que 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 maintenant être aussi ...

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 il vous donne quelques avantages si vous souhaitez passer par la configuration supplémentaire. De toute façon, vous semblez pouvoir tirer le meilleur parti des deux approches.

1
Mauricio Morales

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

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

Supprimez les 2 tables en question (pour prendre l’exemple OP, Member et Comment) dans votre fichier .edmx et rajoutez-les à nouveau via "Générer le modèle à partir de la base de données". (c’est-à-dire ne tentez pas de laisser Visual Studio les mettre à jour - supprimez, enregistrez, ajoutez, enregistrez)

Il créera ensuite un 3ème tableau conforme à ce qui est suggéré ici.

Ceci est pertinent dans les cas où une relation pure plusieurs-à-plusieurs est ajoutée en premier 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 signaler, car il s’agit du lien n ° 1 sur Google, qui cherche le problème, mais vient tout d’abord du côté de la base de données.

0
Andy S

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

Remarque: Dans l'attribut ForeignKey, entre parenthèses et guillemets, placez le nom de la classe à laquelle il est fait référence.

enter image description here

0
Oscar Echaverry