entity-framework-relaciones-muchos-a-muchos

Relaciones muchos a muchos en Entity Framework

  • 4 min

Una relación muchos a muchos (N) ocurre cuando múltiples registros en una tabla pueden asociarse con múltiples registros en otra tabla.

Este tipo de relación es más compleja debido a la necesidad de una tabla intermedia para almacenar las asociaciones (llamada tabla puente o tabla de join).

Vamos a verlo modelando un curso con alumnos (perdón por el topicazo), donde,

public class Estudiante
{
    public int EstudianteId { get; set; }
    public string Nombre { get; set; }
    
    // Propiedad de navegación
    public ICollection<Curso> Cursos { get; set; } = new List<Curso>();
}

public class Curso
{
    public int CursoId { get; set; }
    public string NombreCurso { get; set; }
    
    // Propiedad de navegación
    public ICollection<Estudiante> Estudiantes { get; set; } = new List<Estudiante>();
}

Configuración de la Relación N

En bases de datos relacionales, las relaciones muchos a muchos (N) no pueden existir directamente entre dos tablas.

Para implementarlas, se requiere una tabla intermedia (llamada tabla de puente o join table) que almacene las combinaciones válidas entre ambas entidades.

Podemos dejar que Entity Framework cree esta tabla automáticamente por nosotros, o crear nosotros la Entidad correspondiente, y realizar las asociaciones a mano.

  • Usa tabla implícita si solo necesitas relacionar registros (ej: “el Estudiante X está en el Curso Y”).
  • Usa tabla explícita si necesitas guardar información sobre la relación misma (ej: “cuándo se inscribió” o “qué calificación tiene”).
CaracterísticaTabla ImplícitaTabla Explícita
ComplejidadBajaMedia/Alta
Datos adicionalesNo soportadoSí soportado
Control sobre esquemaLimitadoCompleto
RendimientoBuenoÓptimo
Recomendado paraRelaciones simples NRelaciones con metadatos

Entity Framework Core no soporta Data Annotations directas para relaciones N. Se requiere Fluent API para la configuración completa.

Tabla de unión implícita (automática)

Entity Framework Core (desde la versión 5) puede generar automáticamente esta tabla sin que tengas que crear una clase para ella.

Configuración con Fluent API:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Estudiante>()
        .HasMany(e => e.Cursos)
        .WithMany(c => c.Estudiantes)
        .UsingEntity(j => j.ToTable("EstudianteCursos"));
}

Esta sería la estructura generada en la BD

CREATE TABLE EstudianteCursos (
    EstudiantesEstudianteId INT NOT NULL,
    CursosCursoId INT NOT NULL,
    PRIMARY KEY (EstudiantesEstudianteId, CursosCursoId),
    FOREIGN KEY (EstudiantesEstudianteId) REFERENCES Estudiantes(EstudianteId),
    FOREIGN KEY (CursosCursoId) REFERENCES Cursos(CursoId)
);

Tabla de unión explícita (manual)

Cuando necesitas guardar información adicional en la relación (como metadatos), debes crear una entidad explícita para la tabla de unión.

En el ejemplo anterior, esta tabla explícita podría ser Inscripción,

public class Inscripcion
{
    public int EstudianteId { get; set; }
    public int CursoId { get; set; }
    public DateTime FechaInscripcion { get; set; } // Dato adicional
    public decimal? Calificacion { get; set; }    // Dato adicional
    
    // Propiedades de navegación
    public Estudiante Estudiante { get; set; }
    public Curso Curso { get; set; }
}

Así lo configuraríamos en Fluent API:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Inscripcion>()
        .HasKey(i => new { i.EstudianteId, i.CursoId }); // Clave compuesta
        
    modelBuilder.Entity<Inscripcion>()
        .HasOne(i => i.Estudiante)
        .WithMany(e => e.Inscripciones)
        .HasForeignKey(i => i.EstudianteId);
        
    modelBuilder.Entity<Inscripcion>()
        .HasOne(i => i.Curso)
        .WithMany(c => c.Inscripciones)
        .HasForeignKey(i => i.CursoId);
}

Cuándo usarla

  • Entidad propia en el modelo: Ej: Inscripcion con propiedades adicionales
  • Control total: Puedes agregar campos como FechaInscripcion, Calificacion, etc
  • Más configuración manual: Requiere definir claves foráneas y relaciones en Fluent API

En este ejemplo, el nombre Inscripcion nos ha ido muy bien, porque muestra se equipara muy bien con el concepto.

Pero si no correspondieran con un concepto, también hubiera sido habitual haberlo llamado EstudianteCursos sin más.

Configuraciones avanzadas

Opciones personalizadas para ajustar el comportamiento del modelo de datos en Entity Framework Core.

Nombrar columnas en la tabla de unión implícita

Se define el nombre de las columnas en una tabla de unión generada automáticamente para una relación muchos a muchos.

modelBuilder.Entity<Estudiante>()
    .HasMany(e => e.Cursos)
    .WithMany(c => c.Estudiantes)
    .UsingEntity<Dictionary<string, object>>(
        "EstudianteCursos",
        j => j.HasOne<Curso>().WithMany().HasForeignKey("CursoId"),
        j => j.HasOne<Estudiante>().WithMany().HasForeignKey("EstudianteId")
    );

Configurar eliminación en cascada

Se establece que al eliminar un estudiante, sus inscripciones asociadas también se eliminen automáticamente.

modelBuilder.Entity<Inscripcion>()
    .HasOne(i => i.Estudiante)
    .WithMany(e => e.Inscripciones)
    .HasForeignKey(i => i.EstudianteId)
    .OnDelete(DeleteBehavior.Cascade);

Ejemplos prácticos