Relaciones de varios a varios

Las relaciones de varios a varios se usan cuando cualquier número de entidades de un tipo de entidad está asociado a cualquier número de entidades del mismo tipo de entidad u otro tipo de entidad. Por ejemplo, un elemento Post puede tener muchos elementos Tags asociados, y cada elemento Tag puede estar asociado a cualquier número de elementos Posts.

Descripción de las relaciones de varios a varios

Las relaciones de varios a varios son diferentes de las relaciones de uno a varios y de uno a uno en que no se pueden representar de una manera sencilla mediante una clave externa. En su lugar, se necesita un tipo de entidad adicional para "combinar" los dos lados de la relación. Esto se conoce como "tipo de entidad de combinación" y se asigna a una "tabla de combinación" en una base de datos relacional. Las entidades de este tipo de entidad de combinación contienen pares de valores de clave externa, donde uno de cada par apunta a una entidad de un lado de la relación y el otro apunta a una entidad del otro lado de la relación. Cada entidad de combinación y, por tanto, cada fila de la tabla de combinación, representa una asociación entre los tipos de entidad de la relación.

EF Core puede ocultar el tipo de entidad de combinación y administrarlo en segundo plano. Esto permite que se usen de forma natural las navegaciones de una relación de varios a varios, agregando o quitando entidades de cada lado según sea necesario. Sin embargo, resulta útil comprender lo que sucede en segundo plano para que su comportamiento general, y en particular, la asignación a una base de datos relacional, tenga sentido. Comencemos con una configuración de esquema de base de datos relacional para representar una relación de varios a varios entre publicaciones y etiquetas:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

En este esquema, PostTag es la tabla de combinación. Contiene dos columnas: PostsId, que es una clave externa a la clave principal de la tabla Posts y TagsId, que es una clave externa a la clave principal de la tabla Tags. Por lo tanto, cada fila de esta tabla representa una asociación entre un elemento Post y un elemento Tag.

Una asignación simplista para este esquema en EF Core consta de tres tipos de entidad: uno por cada tabla. Si cada uno de estos tipos de entidad se representa mediante una clase de .NET, esas clases podrían tener el siguiente aspecto:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostsId { get; set; }
    public int TagsId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

Observe que en esta asignación no hay ninguna relación de varios a varios, sino dos relaciones de uno a varios, una para cada una de las claves externas definidas en la tabla de combinación. Esta no es una manera poco razonable de asignar estas tablas, pero no refleja la intención de la tabla de combinación, que es representar una única relación de varios a varios, en lugar de dos relaciones de uno a varios.

EF permite una asignación más natural mediante la introducción de dos navegaciones de colección, una en Post que contiene sus elementos Tags relacionados y un inverso en Tag que contiene sus elementos Posts relacionados. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int PostsId { get; set; }
    public int TagsId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

Sugerencia

Estas nuevas navegaciones se conocen como "navegaciones de omisión", ya que omiten la entidad de combinación para proporcionar acceso directo al otro lado de la relación de varios a varios.

Como se muestra en los ejemplos siguientes, se puede asignar una relación de varios a varios de esta manera, es decir, con una clase de .NET para la entidad de combinación, y con ambas navegaciones para las dos relaciones uno a varios y las navegaciones de omisión expuestas en los tipos de entidad. Sin embargo, EF puede administrar la entidad de combinación de forma transparente, sin definir una clase de .NET para ella y sin navegaciones para las dos relaciones de uno a varios. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

De hecho, las convenciones de creación de modelos de EF asignarán, de forma predeterminada, los tipos Post y Tag que se muestran aquí a las tres tablas del esquema de base de datos de la parte superior de esta sección. Esta asignación, sin uso explícito del tipo de combinación, es lo que normalmente significa el término "varios a varios".

Ejemplos

Las secciones siguientes contienen ejemplos de relaciones de varios a varios, incluida la configuración necesaria para lograr cada asignación.

Sugerencia

El código de todos los ejemplos siguientes se puede encontrar en el archivo OneToMany.cs.

Relación de varios a varios básica

En el caso más básico de una relación de varios a varios, los tipos de entidad de cada extremo de la relación tienen ambos una navegación de colección. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

Esta relación se asigna por convención. Aunque no es necesaria, a continuación se muestra una configuración explícita equivalente para esta relación como herramienta de aprendizaje:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts);
}

Incluso con esta configuración explícita, muchos aspectos de la relación todavía están configurados por convención. Una configuración explícita más completa, de nuevo con fines de aprendizaje, sería:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            "PostTag",
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagsId").HasPrincipalKey(nameof(Tag.Id)),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostsId").HasPrincipalKey(nameof(Post.Id)),
            j => j.HasKey("PostsId", "TagsId"));
}

Importante

No intente configurar completamente todo aunque no sea necesario. Como se puede ver anteriormente, el código se complica rápidamente y es fácil cometer un error. E, incluso en el ejemplo anterior, hay muchas cosas en el modelo que todavía están configuradas por convención. No es realista pensar que todo lo que hay en un modelo de EF siempre se puede configurar completamente explícitamente.

Independientemente de si la relación se crea por convención o mediante cualquiera de las configuraciones explícitas mostradas, el esquema asignado resultante (con SQLite) es:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Sugerencia

Cuando se usa un flujo de Database First para aplicar scaffolding a en elemento DbContext a partir de una base de datos existente, EF Core 6 (y versiones posteriores) busca este patrón en el esquema de base de datos y aplica scaffolding a una relación de varios a varios como se describe en este documento. Este comportamiento se puede cambiar mediante el uso de una plantilla T4 personalizada. Para ver otras opciones, consulte Las relaciones de varios a varios sin entidades de combinación asignadas ahora tienen scaffolding.

Importante

Actualmente, EF Core usa Dictionary<string, object> para representar instancias de entidad de combinación para las que no se ha configurado ninguna clase de .NET. Sin embargo, para mejorar el rendimiento, es posible que se use un tipo diferente en una versión futura de EF Core. No dependa de que el tipo de combinación sea Dictionary<string, object> a menos que se haya configurado explícitamente.

Relación de varios a varios con tabla de combinación con nombre

En el ejemplo anterior, la tabla de combinación se llamaba PostTag por convención. Se puede asignar un nombre explícito con UsingEntity. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity("PostsToTagsJoinTable");
}

Todo lo demás sobre la asignación sigue igual, solo cambia el nombre de la tabla de combinación:

CREATE TABLE "PostsToTagsJoinTable" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostsToTagsJoinTable" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostsToTagsJoinTable_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostsToTagsJoinTable_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Relación de varios a varios con nombres de clave externa de tabla de combinación

A continuación del ejemplo anterior, también se pueden cambiar los nombres de las columnas de clave externa de la tabla de combinación. Existen dos formas de hacerlo. La primera consiste en especificar explícitamente los nombres de propiedad de clave externa en la entidad de combinación. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagForeignKey"),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostForeignKey"));
}

La segunda manera es dejar las propiedades con sus nombres por convención y luego asignar estas propiedades a otros nombres de columna. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            j =>
            {
                j.Property("PostsId").HasColumnName("PostForeignKey");
                j.Property("TagsId").HasColumnName("TagForeignKey");
            });
}

En cualquier caso, la asignación sigue siendo la misma, solo cambian los nombres de columna de clave externa:

CREATE TABLE "PostTag" (
    "PostForeignKey" INTEGER NOT NULL,
    "TagForeignKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
    CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Sugerencia

Aunque no se muestra aquí, los dos ejemplos anteriores se pueden combinar para cambiar el nombre de la tabla de combinación y sus nombres de columna de clave externa.

Relación de varios a varios con una clase para la entidad de combinación

Hasta ahora en los ejemplos, la tabla de combinación se ha asignado automáticamente a un tipo de entidad de tipo compartido. Esto elimina la necesidad de crear una clase dedicada para el tipo de entidad. Sin embargo, puede ser útil tener una clase de este tipo para que se pueda hacer referencia fácilmente, especialmente cuando se agregan navegaciones o una carga a la clase, como se muestra en ejemplos posteriores a continuación. Para ello, cree primero un tipo PostTag para la entidad de combinación además de los tipos existentes para Post y Tag:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
}

Sugerencia

La clase puede tener cualquier nombre, pero es habitual combinar los nombres de los tipos de cada extremo de la relación.

Ahora, se puede usar el método UsingEntity para configurarlo como el tipo de entidad de combinación de la relación. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

Los elementos PostId y TagId se seleccionan automáticamente como claves externas y se configuran como clave principal compuesta del tipo de entidad de combinación. Las propiedades que se van a usar para las claves externas se pueden configurar explícitamente para los casos en los que no coincidan con la convención de EF. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>().WithMany().HasForeignKey(e => e.TagId),
            r => r.HasOne<Post>().WithMany().HasForeignKey(e => e.PostId));
}

El esquema de base de datos asignado para la tabla de combinación de este ejemplo es estructuralmente equivalente a los ejemplos anteriores, pero con algunos nombres de columna diferentes:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Relación de varios a varios con navegaciones a la entidad de combinación

A continuación del ejemplo anterior, ahora que hay una clase que representa la entidad de combinación, resulta fácil agregar navegaciones que hagan referencia a esta clase. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
}

Importante

Como se muestra en este ejemplo, se pueden usar las navegaciones al tipo de entidad de combinación además de las navegaciones de omisión entre los dos extremos de la relación de varios a varios. Esto significa que se pueden usar las navegaciones de omisión para interactuar con la relación de varios a varios de forma natural, mientras que las navegaciones al tipo de entidad de combinación se pueden usar cuando se necesita un mayor control sobre las propias entidades de combinación. En cierto sentido, esta asignación proporciona lo mejor de ambos mundos entre una asignación de varios a varios simple y una asignación que coincide más explícitamente con el esquema de base de datos.

No es necesario cambiar nada en la llamada a UsingEntity, ya que las navegaciones a la entidad de combinación se seleccionan por convención. Por lo tanto, la configuración de este ejemplo es la misma que para el último ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

Las navegaciones se pueden configurar explícitamente para los casos en los que no se pueden determinar por convención. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>().WithMany(e => e.PostTags),
            r => r.HasOne<Post>().WithMany(e => e.PostTags));
}

El esquema de base de datos asignado no se ve afectado por la inclusión de las navegaciones en el modelo:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Relación de varios a varios con navegaciones hacia y desde la entidad de combinación

En el ejemplo anterior, se agregaron navegaciones al tipo de entidad de combinación desde los tipos de entidad de cada extremo de la relación de varios a varios. Las navegaciones también se pueden agregar en la otra dirección o en ambas direcciones. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

No es necesario cambiar nada en la llamada a UsingEntity, ya que las navegaciones a la entidad de combinación se seleccionan por convención. Por lo tanto, la configuración de este ejemplo es la misma que para el último ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

Las navegaciones se pueden configurar explícitamente para los casos en los que no se pueden determinar por convención. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags));
}

El esquema de base de datos asignado no se ve afectado por la inclusión de las navegaciones en el modelo:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Relación de varios a varios con navegaciones y claves externas cambiadas

En el ejemplo anterior, se mostró una relación de varios a varios con navegaciones hacia y desde el tipo de entidad de combinación. Este ejemplo es el mismo, salvo que también se cambian las propiedades de clave externa utilizadas. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostForeignKey { get; set; }
    public int TagForeignKey { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

De nuevo, se usa el método UsingEntity para configurar esto:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasForeignKey(e => e.TagForeignKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasForeignKey(e => e.PostForeignKey));
}

Ahora, el esquema de base de datos asignado es:

CREATE TABLE "PostTag" (
    "PostForeignKey" INTEGER NOT NULL,
    "TagForeignKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
    CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Relación de varios a varios unidireccional

Nota

En EF Core 7, se presentaron las relaciones de varios a varios unidireccionales. En versiones anteriores, se podía usar una navegación privada como solución alternativa.

No es necesario incluir una navegación en ambos lados de la relación de varios a varios. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
}

EF necesita alguna configuración para saber que se trata de una relación de varios a varios, en lugar de una relación de uno a varios. Esto se hace mediante HasMany y WithMany, pero no se pasa ningún argumento en el lado sin navegación. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany();
}

Quitar la navegación no afecta al esquema de base de datos:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Relación de varios a varios y tabla de combinación con carga

En los ejemplos hasta este punto, la tabla de combinación solo se ha usado para almacenar los pares de claves externas que representan cada asociación. Sin embargo, también se puede usar para almacenar información sobre la asociación, por ejemplo, el momento en el que se creó. En tales casos, es mejor definir un tipo para la entidad de combinación y agregar las propiedades de "carga de asociación" a este tipo. También es habitual crear navegaciones a la entidad de combinación además de las "navegaciones de omisión" utilizadas para la relación de varios a varios. Estas navegaciones adicionales permiten hacer referencia a la entidad de combinación fácilmente desde el código, lo que facilita la lectura y el cambio de los datos de la carga. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public DateTime CreatedOn { get; set; }
}

También es habitual usar valores generados para las propiedades de la carga; por ejemplo, una marca de tiempo de base de datos que se establece automáticamente cuando se inserta la fila de asociación. Esto requiere una configuración mínima. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

El resultado se asigna a un esquema de tipo de entidad con una marca de tiempo establecida automáticamente cuando se inserta una fila:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Sugerencia

El código SQL que se muestra aquí es para SQLite. En SQL Server/Azure SQL, use .HasDefaultValueSql("GETUTCDATE()") y para TEXT lea datetime.

Tipo de entidad de tipo compartido personalizado como entidad de combinación

En el ejemplo anterior, se usó el tipo PostTag como tipo de entidad de combinación. Este tipo es específico de la relación entre publicaciones y etiquetas. Sin embargo, si tiene varias tablas de combinación con la misma forma, se puede usar el mismo tipo CLR para todas ellas. Por ejemplo, imagine que todas las tablas de combinación tienen la columna CreatedOn. Se pueden asignar mediante la clase JoinType asignada como un tipo de entidad de tipo compartido:

public class JoinType
{
    public int Id1 { get; set; }
    public int Id2 { get; set; }
    public DateTime CreatedOn { get; set; }
}

A continuación, varias relaciones de varios a varios diferentes pueden hacer referencia a este tipo como tipo de entidad de combinación. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<JoinType> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<JoinType> PostTags { get; } = [];
}

public class Blog
{
    public int Id { get; set; }
    public List<Author> Authors { get; } = [];
    public List<JoinType> BlogAuthors { get; } = [];
}

public class Author
{
    public int Id { get; set; }
    public List<Blog> Blogs { get; } = [];
    public List<JoinType> BlogAuthors { get; } = [];
}

Y estas relaciones se pueden configurar adecuadamente para asignar el tipo de combinación a una tabla diferente para cada relación:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<JoinType>(
            "PostTag",
            l => l.HasOne<Tag>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id1),
            r => r.HasOne<Post>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));

    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Authors)
        .WithMany(e => e.Blogs)
        .UsingEntity<JoinType>(
            "BlogAuthor",
            l => l.HasOne<Author>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id1),
            r => r.HasOne<Blog>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

Esto da como resultado las tablas siguientes en el esquema de base de datos:

CREATE TABLE "BlogAuthor" (
    "Id1" INTEGER NOT NULL,
    "Id2" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_BlogAuthor" PRIMARY KEY ("Id1", "Id2"),
    CONSTRAINT "FK_BlogAuthor_Authors_Id1" FOREIGN KEY ("Id1") REFERENCES "Authors" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_BlogAuthor_Blogs_Id2" FOREIGN KEY ("Id2") REFERENCES "Blogs" ("Id") ON DELETE CASCADE);


CREATE TABLE "PostTag" (
    "Id1" INTEGER NOT NULL,
    "Id2" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("Id1", "Id2"),
    CONSTRAINT "FK_PostTag_Posts_Id2" FOREIGN KEY ("Id2") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_Id1" FOREIGN KEY ("Id1") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Relación de varios a varios con claves alternativas

Hasta ahora, todos los ejemplos han mostrado las claves externas en el tipo de entidad de combinación restringidas a las claves principales de los tipos de entidad en cualquier lado de la relación. En su lugar, cada clave externa, o ambas, se puede restringir a una clave alternativa. Por ejemplo, considere este modelo en el que Tag y Post tienen propiedades de clave alternativas:

public class Post
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Post> Posts { get; } = [];
}

La configuración de este modelo es:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasPrincipalKey(nameof(Tag.AlternateKey)),
            r => r.HasOne(typeof(Post)).WithMany().HasPrincipalKey(nameof(Post.AlternateKey)));
}

Y el esquema de base de datos resultante, para mayor claridad, incluidas también las tablas con las claves alternativas:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "PostTag" (
    "PostsAlternateKey" INTEGER NOT NULL,
    "TagsAlternateKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsAlternateKey", "TagsAlternateKey"),
    CONSTRAINT "FK_PostTag_Posts_PostsAlternateKey" FOREIGN KEY ("PostsAlternateKey") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsAlternateKey" FOREIGN KEY ("TagsAlternateKey") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);

La configuración para usar claves alternativas es ligeramente diferente si el tipo de entidad de combinación está representado por un tipo de .NET. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

Ahora, la configuración puede usar el método genérico UsingEntity<>:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey));
}

Y el esquema resultante es:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);

Relación de varios a varios y tabla de combinación con clave principal independiente

Hasta ahora, el tipo de entidad de combinación de todos los ejemplos tiene una clave principal compuesta por las dos propiedades de clave externa. Esto se debe a que cada combinación de valores de estas propiedades puede ocurrir como máximo una vez. Por lo tanto, estas propiedades forman una clave principal natural.

Nota

EF Core no admite entidades duplicadas en ninguna navegación de colección.

Si controla el esquema de la base de datos, no hay ninguna razón para que la tabla de combinación tenga una columna de clave principal adicional; sin embargo, es posible que una tabla de combinación existente tenga definida una columna de clave principal. EF también puede hacer esta asignación con alguna configuración.

Es posible que sea más fácil crear una clase para representar la entidad de combinación. Por ejemplo:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int Id { get; set; }
    public int PostId { get; set; }
    public int TagId { get; set; }
}

Ahora, esta propiedad PostTag.Id se selecciona como clave principal por convención, por lo que la única configuración necesaria es una llamada a UsingEntity para el tipo PostTag:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

Y el esquema resultante para la tabla de combinación es:

CREATE TABLE "PostTag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

También se puede agregar una clave principal a la entidad de combinación sin definir una clase para ella. Por ejemplo, solo con los tipos Post y Tag:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

La clave se puede agregar con esta configuración:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            j =>
            {
                j.IndexerProperty<int>("Id");
                j.HasKey("Id");
            });
}

Lo que da como resultado una tabla de combinación con una columna de clave principal independiente:

CREATE TABLE "PostTag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

Relación de varios a varios sin eliminación en cascada

En todos los ejemplos mostrados anteriormente, las claves externas creadas entre la tabla de combinación y los dos lados de la relación de varios a varios se crean con el comportamiento de eliminación en cascada. Esto es muy útil porque significa que si se elimina una entidad de cualquier lado de la relación, se eliminan automáticamente las filas de la tabla de combinación de esa entidad. O bien, en otras palabras, cuando una entidad ya no existe, ya no existen sus relaciones con otras entidades.

Es difícil imaginar cuándo resulta útil cambiar este comportamiento, pero se puede hacer si lo desea. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().OnDelete(DeleteBehavior.Restrict),
            r => r.HasOne(typeof(Post)).WithMany().OnDelete(DeleteBehavior.Restrict));
}

El esquema de base de datos de la tabla de combinación usa el comportamiento de eliminación restringida en la restricción de clave externa:

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE RESTRICT,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE RESTRICT);

Relación de varios a varios con referencia propia

Se puede usar el mismo tipo de entidad en ambos extremos de una relación de varios a varios; esto se conoce como una relación de "referencia propia". Por ejemplo:

public class Person
{
    public int Id { get; set; }
    public List<Person> Parents { get; } = [];
    public List<Person> Children { get; } = [];
}

Esto se asigna a una tabla de combinación llamada PersonPerson, con ambas claves externas apuntando a la tabla People:

CREATE TABLE "PersonPerson" (
    "ChildrenId" INTEGER NOT NULL,
    "ParentsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PersonPerson" PRIMARY KEY ("ChildrenId", "ParentsId"),
    CONSTRAINT "FK_PersonPerson_People_ChildrenId" FOREIGN KEY ("ChildrenId") REFERENCES "People" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PersonPerson_People_ParentsId" FOREIGN KEY ("ParentsId") REFERENCES "People" ("Id") ON DELETE CASCADE);

Relación de varios a varios con referencia propia simétrica

A veces, una relación de varios a varios es simétrica de forma natural. Es decir, si la entidad A está relacionada con la entidad B, la entidad B también está relacionada con la entidad A. Esto se modela de forma natural mediante una sola navegación. Por ejemplo, imagine el caso en el que la persona A es amiga de la persona B, entonces la persona B es amiga de la persona A:

public class Person
{
    public int Id { get; set; }
    public List<Person> Friends { get; } = [];
}

Desafortunadamente, esto no es fácil de asignar. No se puede usar la misma navegación para ambos extremos de la relación. Lo mejor que se puede hacer es asignarlo como una relación de varios a varios unidireccional. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasMany(e => e.Friends)
        .WithMany();
}

Sin embargo, para asegurarse de que dos personas estén ambas relacionadas entre sí, se tendrá que agregar manualmente cada persona a la colección Friends de la otra persona. Por ejemplo:

ginny.Friends.Add(hermione);
hermione.Friends.Add(ginny);

Uso directo de la tabla de combinación

Todos los ejemplos anteriores usan los patrones de asignación de varios a varios de EF Core. Sin embargo, también es posible asignar una tabla de combinación a un tipo de entidad normal y simplemente usar las dos relaciones de uno a varios para todas las operaciones.

Por ejemplo, estos tipos de entidad representan la asignación de dos tablas normales y una tabla de combinación sin usar ninguna relación de varios a varios:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = new();
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = new();
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

Esto no requiere ninguna asignación especial, ya que son tipos de entidad normales con relaciones de uno a varios normales.

Recursos adicionales