Cree primero el código, muchos a muchos, con campos adicionales en la tabla de asociación

298

Tengo este escenario:

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

¿Cómo configuro mi asociación con una API fluida ? ¿O hay una mejor manera de crear la tabla de asociación?

hgdean
fuente

Respuestas:

524

No es posible crear una relación de muchos a muchos con una tabla de unión personalizada. En una relación de muchos a muchos, EF administra la tabla de unión de forma interna y oculta. Es una tabla sin una clase de entidad en su modelo. Para trabajar con una tabla de unión con propiedades adicionales, tendrá que crear dos relaciones uno a muchos. Podría verse así:

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 ahora desea encontrar todos los comentarios de miembros con LastName= "Smith", por ejemplo, puede escribir una consulta como esta:

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

O para crear una lista de miembros con el nombre "Smith" (suponemos que hay más de uno) junto con sus comentarios, puede usar una proyección:

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

Si desea encontrar todos los comentarios de un miembro con MemberId= 1:

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

Ahora también puede filtrar por las propiedades en su tabla de unión (que no sería posible en una relación de muchos a muchos), por ejemplo: Filtre todos los comentarios del miembro 1 que tienen una propiedad 99 en Something:

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

Debido a la carga lenta, las cosas podrían volverse más fáciles. Si tiene una carga Member, debería poder obtener los comentarios sin una consulta explícita:

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

Supongo que la carga diferida buscará los comentarios automáticamente detrás de escena.

Editar

Solo por diversión, algunos ejemplos más sobre cómo agregar entidades y relaciones y cómo eliminarlas en este modelo:

1) Cree un miembro y dos comentarios de este miembro:

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) Agregue un tercer comentario 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) Crear un nuevo miembro y relacionarlo con el comentario 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) Crear una relación entre el miembro2 existente y el comentario3:

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) Eliminar esta relación nuevamente:

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) Eliminar member1 y todas sus relaciones con los comentarios:

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

Esto también elimina las relaciones MemberCommentsporque las relaciones uno a muchos entre MemberyMemberComments y entre Commenty MemberCommentsestán configurados con la eliminación en cascada por convención. Y este es el caso porque MemberIdy CommentIden MemberCommentse detectan como propiedades de clave externa para las propiedades de navegación Membery Comment, y dado que las propiedades FK son de tipo no anulable, intse requiere la relación que finalmente causa la configuración de eliminación en cascada. Tiene sentido en este modelo, creo.

Slauma
fuente
1
Gracias. Agradecemos mucho la información adicional que proporcionó.
hgdean
77
@hgdean: He enviado spam algunos ejemplos más, lo siento, pero es un modelo interesante y las preguntas sobre muchos a muchos con datos adicionales en la tabla de unión ocurren de vez en cuando aquí. Ahora para la próxima vez tengo algo con lo que vincularme ... :)
Slauma
44
@Esteban: no se anula OnModelCreating. El ejemplo solo se basa en convenciones de mapeo y anotaciones de datos.
Slauma
44
Nota: si usa este enfoque sin Fluent API, asegúrese de verificar en su base de datos que solo tiene una clave compuesta con MemberIdy CommentIdcolumnas y no una tercera columna adicional Member_CommentId(o algo así), lo que significa que no tenía nombres coincidentes exactos a través de objetos para sus llaves
Simon_Weaver
3
@Simon_Weaver (o cualquiera que conozca la respuesta) Tengo una situación similar pero me gustaría tener la clave principal "MemberCommentID" para esa tabla, ¿es esto posible o no? Actualmente estoy recibiendo una excepción, por favor, eche un vistazo a mi pregunta, realmente necesito ayuda ... stackoverflow.com/questions/26783934/…
duxfox--
98

Excelente respuesta de Slauma.

Solo publicaré el código para hacer esto usando la asignación fluida de 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; }
}

En su DbContextclase derivada, podría hacer esto:

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

Tiene el mismo efecto que la respuesta aceptada, con un enfoque diferente, que no es ni mejor ni peor.

EDITAR: he cambiado CreatedDate de bool a DateTime.

EDIT 2: debido a la falta de tiempo, he puesto un ejemplo de una aplicación en la que estoy trabajando para asegurarme de que esto funcione.

Esteban
fuente
1
Creo que esto está mal. Está creando una relación M: M aquí donde debe ser 1: M para ambas entidades.
CHS
1
@CHS In your classes you can easily describe a many to many relationship with properties that point to each other.tomado de: msdn.microsoft.com/en-us/data/hh134698.aspx . Julie Lerman no puede estar equivocada.
Esteban
1
Esteban, el mapeo de relaciones es realmente incorrecto. @CHS tiene razón sobre esto. Julie Lerman está hablando de una relación "verdadera" de muchos a muchos, mientras que aquí tenemos un ejemplo de un modelo que no se puede mapear como muchos a muchos. Su mapeo incluso no se compilará porque no tiene una Commentspropiedad Member. Y no puede simplemente solucionar esto cambiando el nombre de la HasManyllamada a MemberCommentsporque la MemberCommententidad no tiene una colección inversa para WithMany. De hecho, debe configurar dos relaciones uno a muchos para obtener la asignación correcta.
Slauma
2
Gracias. Seguí esta solución para hacer el mapeo de muchos a muchos.
Thomas.Benz
No lo sé, pero esto funciona mejor con MySql. Sin el constructor, Mysql me arrojó un error cuando intenté la migración.
Rodrigo Prieto
11

@Esteban, el código que proporcionó es correcto, gracias, pero incompleto, lo probé. Faltan propiedades en la clase "UserEmail":

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

Publico el código que he probado si alguien está interesado. Saludos

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

Quiero proponer una solución donde se puedan lograr ambos sabores de una configuración de muchos a muchos.

El "problema" es que necesitamos crear una vista que se dirija a la Tabla de unión, ya que EF valida que la tabla de un esquema se pueda asignar como máximo una vez por cada EntitySet.

Esta respuesta se suma a lo que ya se ha dicho en las respuestas anteriores y no anula ninguno de esos enfoques, se basa en ellos.

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

La configuración:

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

El 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) respuesta

Si ahora desea encontrar todos los comentarios de miembros con LastName = "Smith", por ejemplo, puede escribir una consulta como esta:

Esto todavía funciona ...

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

... pero ahora también podría ser ...

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

O para crear una lista de miembros con el nombre "Smith" (suponemos que hay más de uno) junto con sus comentarios, puede usar una proyección:

Esto todavía funciona ...

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

... pero ahora también podría ser ...

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

Si quieres eliminar un comentario de un miembro

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

member.Comments.Remove(comment);

Si quieres Include()los comentarios de un miembro

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

Todo esto se siente como el azúcar sintáctico, sin embargo, le da algunas ventajas si está dispuesto a pasar por la configuración adicional. De cualquier manera, parece ser capaz de obtener lo mejor de ambos enfoques.

Mauricio Morales
fuente
Aprecio la mayor legibilidad al escribir las consultas LINQ. Puede que solo tenga que adoptar este método. Tengo que preguntar, ¿EF EntitySet también actualiza automáticamente la vista en la base de datos? ¿Estaría de acuerdo en que esto parece similar al [Sugar] como se describe en el plan EF5.0? github.com/dotnet/EntityFramework.Docs/blob/master/…
Krptodr
Me pregunto por qué parece redefinir EntityTypeConfiguration<EntityType>la clave y las propiedades del tipo de entidad. Por ejemplo, Property(x => x.MemberID).HasColumnType("int").IsRequired();parece ser redundante con public int MemberID { get; set; }. ¿Podría aclarar mi confusa comprensión, por favor?
minutos
0

TLDR; (semi-relacionado con un error del editor EF en EF6 / VS2012U5) si genera el modelo desde la base de datos y no puede ver la tabla m: m atribuida: elimine las dos tablas relacionadas -> Guardar .edmx -> Generar / agregar desde la base de datos - > Guardar.

Para aquellos que vinieron aquí preguntándose cómo obtener una relación de muchos a muchos con columnas de atributos para mostrar en el archivo EF .edmx (ya que actualmente no se mostraría y sería tratado como un conjunto de propiedades de navegación), Y generó estas clases de su tabla de base de datos (o base de datos primero en la jerga de MS, creo).

Elimine las 2 tablas en cuestión (para tomar el ejemplo de OP, Miembro y Comentario) en su .edmx y agréguelas nuevamente a través de 'Generar modelo a partir de la base de datos'. (es decir, no intente dejar que Visual Studio los actualice: eliminar, guardar, agregar, guardar)

Luego creará una tercera tabla en línea con lo que se sugiere aquí.

Esto es relevante en los casos en que se agrega una relación pura de muchos a muchos al principio, y los atributos se diseñan en la base de datos más adelante.

Esto no fue aclarado inmediatamente de este hilo / Google. Así que solo lo expongo, ya que este es el enlace n. ° 1 en Google buscando el problema, pero primero viene del lado de la base de datos.

Andy S
fuente
0

Una forma de resolver este error es colocar el ForeignKeyatributo en la parte superior de la propiedad que desea como clave externa y agregar la propiedad de navegación.

Nota: En el ForeignKeyatributo, entre paréntesis y comillas dobles, coloque el nombre de la clase mencionada de esta manera.

ingrese la descripción de la imagen aquí

Oscar Echaverry
fuente
Agregue una explicación mínima en la respuesta, ya que el enlace proporcionado puede no estar disponible en el futuro.
n4m31ess_c0d3r
2
Debe ser el nombre de la propiedad de navegación , en lugar de la clase.
aaron