Crie primeiro o código, muitos para muitos, com campos adicionais na tabela de associação

298

Eu tenho este cenário:

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

Como configuro minha associação com a API fluente ? Ou existe uma maneira melhor de criar a tabela de associação?

hgdean
fonte

Respostas:

524

Não é possível criar um relacionamento muitos-para-muitos com uma tabela de junção personalizada. Em um relacionamento muitos para muitos, a EF gerencia a tabela de junção internamente e oculta. É uma tabela sem uma classe Entity no seu modelo. Para trabalhar com uma tabela de junção com propriedades adicionais, você precisará criar dois relacionamentos um para muitos. Pode ficar assim:

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 agora você deseja encontrar todos os comentários de membros com LastName= "Smith", por exemplo, você pode escrever uma consulta como esta:

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, para criar uma lista de membros com o nome "Smith" (presumimos que exista mais de um), juntamente com seus comentários, você pode usar uma projeção:

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

Se você deseja encontrar todos os comentários de um membro com MemberId= 1:

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

Agora você também pode filtrar pelas propriedades em sua tabela de junção (o que não seria possível em um relacionamento muitos para muitos), por exemplo: Filtre todos os comentários do membro 1 que possui uma propriedade 99 Something:

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

Devido ao carregamento lento, as coisas podem ficar mais fáceis. Se você tiver um carregamento Member, poderá obter os comentários sem uma consulta explícita:

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

Eu acho que esse carregamento lento buscará os comentários automaticamente nos bastidores.

Editar

Apenas por diversão, alguns exemplos mais sobre como adicionar entidades e relacionamentos e como excluí-los neste modelo:

1) Crie um membro e dois comentários deste 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) Adicione um terceiro comentário de 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) Crie um novo membro e relacione-o com o comentário existente2:

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) Crie um relacionamento entre o membro2 existente e o comentário3:

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) Exclua esse relacionamento novamente:

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) Exclua member1 e todos os seus relacionamentos com os comentários:

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

Isso exclui as relações em MemberCommentsdemasiado porque as relações um-para-muitos entre Membere MemberCommentse entre Commente MemberCommentssão configurados com eliminação em cascata por convenção. E esse é o caso, porque MemberIde CommentIdin MemberCommentsão detectados como propriedades de chave estrangeira para as propriedades Membere de Commentnavegação e, como as propriedades FK são do tipo não nulo, inté necessário o relacionamento que finalmente causa a configuração de exclusão em cascata. Faz sentido nesse modelo, eu acho.

Slauma
fonte
1
Obrigado. Aprecio muito as informações adicionais que você forneceu.
precisa saber é o seguinte
7
@ hgdean: enviei mais alguns exemplos com spam, desculpe, mas é um modelo interessante e perguntas sobre muitos para muitos com dados adicionais na tabela de junção ocorrem de vez em quando aqui. Agora, para a próxima vez que eu tenho algo a ligação com ... :)
Slauma
4
@ Esteban: Não há substituído OnModelCreating. O exemplo depende apenas de convenções de mapeamento e anotações de dados.
Slauma
4
Nota: se você usar essa abordagem sem Fluente make API-se de verificar em seu banco de dados que você só tem uma chave composta com MemberIde CommentIdcolunas e não uma terceira coluna adicional Member_CommentId(ou algo parecido) - o que significa que você não tem nomes de correspondência exata objetos através de suas chaves
Simon_Weaver
3
@Simon_Weaver (ou alguém que possa saber a resposta) Estou com uma situação semelhante, mas gostaria de ter a chave primária "MemberCommentID" para essa tabela, isso é possível ou não? Atualmente estou recebendo uma exceção, por favor dê uma olhada na minha pergunta, eu realmente preciso de ajuda ... stackoverflow.com/questions/26783934/...
duxfox--
97

Excelente resposta de Slauma.

Vou postar o código para fazer isso usando o mapeamento de 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; }
}

Na sua DbContextclasse derivada, você pode fazer o seguinte:

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

Tem o mesmo efeito que a resposta aceita, com uma abordagem diferente, que não é nem melhor nem pior.

Edição: Eu mudei CreatedDate de bool para DateTime.

EDIÇÃO 2: Devido à falta de tempo, coloquei um exemplo de um aplicativo no qual estou trabalhando para garantir que isso funcione.

Esteban
fonte
1
Acho que isso está errado. Você está criando um relacionamento M: M aqui onde ele precisa ser 1: M para ambas as entidades.
CHS
1
@CHS In your classes you can easily describe a many to many relationship with properties that point to each other.retirado de: msdn.microsoft.com/en-us/data/hh134698.aspx . Julie Lerman não pode estar errada.
Esteban
1
Esteban, o mapeamento de relacionamento está realmente incorreto. A @CHS está certa sobre isso. Julie Lerman está falando sobre um "verdadeiro" relacionamento muitos-para-muitos, enquanto temos aqui um exemplo para um modelo que não pode ser mapeado como muitos-para-muitos. Seu mapeamento ainda não será compilado porque você não possui uma Commentspropriedade Member. E você não pode simplesmente corrigir isso renomeando a HasManychamada para, MemberCommentsporque a MemberCommententidade não possui uma coleção inversa WithMany. Na verdade, você precisa configurar dois relacionamentos um para muitos para obter o mapeamento correto.
Slauma
2
Obrigado. Eu segui esta solução para fazer o mapeamento de muitos para muitos.
precisa saber é o seguinte
Eu não sei, mas isso funciona melhor com o MySql. Sem o construtor, o Mysql me gerou um erro quando tentei a migração.
Rodrigo Prieto
11

@Esteban, o código que você forneceu está correto, obrigado, mas incompleto, eu testei. Faltam propriedades na classe "UserEmail":

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

Publico o código que testei se alguém estiver interessado. Saudações

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

Quero propor uma solução em que ambos os sabores de uma configuração muitos-para-muitos possam ser alcançados.

O "problema" é que precisamos criar uma exibição que tenha como alvo a tabela de junção, pois o EF valida que a tabela de um esquema pode ser mapeada no máximo uma vez por EntitySet.

Essa resposta se soma ao que já foi dito nas respostas anteriores e não substitui nenhuma dessas abordagens, ela se baseia nelas.

O modelo:

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

A configuração:

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

O contexto:

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

A partir de Salumã (@Saluma) resposta

Se agora você deseja encontrar todos os comentários de membros com Sobrenome = "Smith", por exemplo, você pode escrever uma consulta como esta:

Isso ainda funciona ...

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

... mas agora também pode ser ...

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

Ou, para criar uma lista de membros com o nome "Smith" (presumimos que exista mais de um), juntamente com seus comentários, você pode usar uma projeção:

Isso ainda funciona ...

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

... mas agora também pode ser ...

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

Se você deseja remover um comentário de um membro

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

member.Comments.Remove(comment);

Se você quiser Include()comentar os membros

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

Tudo isso parece açúcar sintático, no entanto, você recebe algumas vantagens se você estiver disposto a passar pela configuração adicional. De qualquer maneira, você parece conseguir o melhor de ambas as abordagens.

Mauricio Morales
fonte
Aprecio a legibilidade aumentada ao digitar as consultas LINQ. Talvez eu tenha que adotar esse método. Preciso perguntar: o EF EntitySet também atualiza automaticamente a exibição no banco de dados? Você concorda que isso parece semelhante ao [Sugar], conforme descrito no plano EF5.0? github.com/dotnet/EntityFramework.Docs/blob/master/…
Krptodr
Gostaria de saber por que você parece redefinir EntityTypeConfiguration<EntityType>a chave e as propriedades do tipo de entidade. Por exemplo, Property(x => x.MemberID).HasColumnType("int").IsRequired();parece ser redundante com public int MemberID { get; set; }. Você poderia esclarecer meu entendimento confuso, por favor?
mins
0

TLDR; (semi-relacionado a um erro do editor EF no EF6 / VS2012U5) se você gerar o modelo a partir do DB e não puder ver a tabela m: m atribuída: exclua as duas tabelas relacionadas -> Salvar .edmx -> Gerar / adicionar do banco de dados - > Salvar.

Para aqueles que vieram aqui se perguntando como obter um relacionamento muitos-para-muitos com colunas de atributo para serem mostrados no arquivo .edmx EF (como atualmente não seria exibido e tratado como um conjunto de propriedades de navegação), E você gerou essas classes da sua tabela de banco de dados (ou banco de dados primeiro no jargão da Microsoft, acredito).

Exclua as 2 tabelas em questão (para pegar o exemplo do OP, Membro e Comentário) no seu .edmx e adicione-as novamente através de 'Gerar modelo do banco de dados'. (ou seja, não tente permitir que o Visual Studio os atualize - exclua, salve, adicione, salve)

Em seguida, ele criará uma terceira tabela de acordo com o que é sugerido aqui.

Isso é relevante nos casos em que um relacionamento entre muitos e muitos é adicionado inicialmente e os atributos são projetados no banco de dados posteriormente.

Isso não ficou claro imediatamente neste tópico / no Google. Então, basta colocá-lo lá fora, já que este é o link 1 no Google, procurando o problema, mas vindo primeiro do lado do banco de dados.

Andy S
fonte
0

Uma maneira de resolver esse erro é colocar o ForeignKeyatributo em cima da propriedade que você deseja como chave estrangeira e adicionar a propriedade de navegação.

Nota: No ForeignKeyatributo, entre parênteses e aspas duplas, coloque o nome da classe referida dessa maneira.

insira a descrição da imagem aqui

Oscar Echaverry
fonte
Adicione uma explicação mínima à resposta, pois o link fornecido pode estar indisponível no futuro.
N4m31ess_c0d3r
2
Deve ser o nome da propriedade de navegação , e não a classe.
precisa