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


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 fluent API? O hay una mejor manera de crear la tabla de asociación?

Author: Peter Mortensen, 2011-08-13

6 answers

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 internamente 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 realmente dos relaciones de 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 los miembros con LastName = "Smith", por ejemplo, puede escribir una consulta así:

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 se puede utilizar 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 de su tabla de unión (lo que no sería posible en una relación muchos a muchos), por ejemplo: Filtrar todos los comentarios del miembro 1 que tienen una propiedad 99 in Something:

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

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

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

Supongo que la carga lenta obtendrá los comentarios automáticamente entre bastidores.

Editar

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

1) Crear 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) Añadir 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 los miembros existentes2 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) Borrar esta relación de nuevo:

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 relacioneships a los comentarios:

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

Esto elimina las relaciones en MemberComments también porque las relaciones de uno a muchos entre Member y MemberComments y entre Comment y MemberComments se configuran con eliminación en cascada por convención. Y este es el caso porque MemberId y CommentId en MemberComment se detectan como propiedades de clave foránea para las propiedades de navegación Member y Comment y como las propiedades FK son de tipo non-nullable int se requiere la relación que finalmente causa el cascading-delete-setup. Tiene sentido en este modelo, creo.

 472
Author: Slauma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-08-14 13:05:35

Excelente respuesta de Slauma.

Solo publicaré el código para hacer esto usando la asignación fluent 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 tu clase derivada DbContext podrías 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 es no mejor ni peor.

EDITAR: He cambiado CreatedDate de bool a DateTime.

[3] EDITAR 2: Debido a la falta de tiempo he colocado un ejemplo de una aplicación Estoy trabajando para asegurarme de que esto funcione.
 87
Author: Esteban,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-07 11:17:37

@Esteban, el código que proporcionaste es correcto, gracias, pero incompleto, lo he probado. 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. Regards

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
 10
Author: LeonardoX,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-03 10:04:11

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 EF .edmx (ya que actualmente no se mostraría y se trataría como un conjunto de propiedades de navegación), Y generaste estas clases a partir de tabla de base de datos (o base de datos-primero en la jerga de MS, creo.)

Eliminar las 2 tablas en cuestión (para tomar el ejemplo OP, Miembro y Comentario) en su .edmx y añadirlos de nuevo a través de 'Generar modelo de base de datos'. (es decir, no intente dejar que Visual Studio los actualice - eliminar, guardar, agregar, guardar)

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

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

Esto no fue inmediatamente claro de este hilo/Googlear. Así que solo ponlo ahí, ya que este es el enlace #1 en Google buscando el problema, pero viniendo del lado de la base de datos primero.

 0
Author: Andy S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-03 10:27:53

Una forma de resolver este error es poner el atributo ForeignKey encima de la propiedad que desea como clave foránea y agregar la propiedad de navegación.

Nota: En el atributo ForeignKey, entre paréntesis y comillas dobles, coloque el nombre de la clase a la que se refiere de esta manera.

introduzca la descripción de la imagen aquí

 0
Author: Oscar Echaverry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-29 18:08:34

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

El "catch" es que necesitamos crear una vista que apunte a la tabla de combinación, ya que EF valida que la tabla de un esquema se pueda mapear como máximo una vez por EntitySet.

Esta respuesta se suma a lo que ya se ha dicho en 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; }
}

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

De la respuesta de Saluma (@Saluma)

Si ahora desea encontrar todos los comentarios de los 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 serlo...

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 se puede utilizar un 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 serlo...

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

Si desea 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 azúcar sintáctico, sin embargo, te da algunas ventajas si estás dispuesto a pasar por la configuración adicional. De cualquier manera parece ser capaz de obtener lo mejor de ambos enfoques.

 0
Author: Mauricio Morales,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-19 17:26:27