DEV Community

Cover image for EF Core 5 Many to Many - Eliminando a tabela de junção em relacionamento de muitos para muitos
Fábio César de Almeida
Fábio César de Almeida

Posted on

4 2

EF Core 5 Many to Many - Eliminando a tabela de junção em relacionamento de muitos para muitos

Este é um exemplo de como criar um relacionamento many to many sem a necessidade de criar uma entidade de junção, e podendo mapear para uma tabela de banco de dados de forma explícita.

Novidade Entity Framework Core 5

EF Core 5 trouxe um recurso bastante interessante e esperado por muitos programadores, e ao mesmo tempo, ainda não é muito conhecido. Se fizermos uma pesquisa sobre o assunto quase não vamos encontrar exemplos de como fazer um relacionamento many to many sem precisar usar uma entidade de junção usando fluent API para fazer o mapeamento para a tabela do banco de dados.

Sobre o exemplo

O exemplo apresentado mostra como criar e fazer um relacionamento de muitos para muitos entre usuários e grupo de usuário, onde um usuário pode participar de mais de um grupo e os grupos podem ter mais de um usuário. Em outras palavras um clássico relacionamento de muitos para muitos.

EFCore3.Many2Many

Esse projeto usa a versão 3 do EF Core, e implementa uma tabela de junção para fazer o relacionamento entre os usuários e grupos. O exemplo funciona nas versões mais recentes também, como a 5 e 6.

public class User
{
    public int UserID { get; set; }
    public string UserName { get; set; }

    public virtual ICollection<UserGroup> UserGroup { get; set; } = new List<UserGroup>();
}

public class Group
{
   public int GroupID { get; set; }
   public string Name { get; set; }

   public virtual ICollection<UserGroup> UserGroup { get; set; } = new List<UserGroup>();
}
public class UserGroup
{
   public int UserID { get; set; }
   public int GroupID { get; set; }

   public virtual Group Group { get; set; }
   public virtual User User { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Observe que temos três entidades, uma que abstrai o usuário, outra o grupo de usuário e uma terceira que faz a junção de muitos para muitos. As entidades usuário e grupo contém uma coleção de usuários e grupos (UserGroup) que é um relacionamento com a entidade de junção.
Embora isso funcione, não é bem isso que esperamos de um ORM. O ideal é que ele de alguma forma faça a junção de forma implícita, sem a necessidade de criar uma entidade só para este fim.
Para definir o nome da tabela de junção (do banco de dados), as FKs e o nome delas, usando fluente API, podemos fazer da seguinte forma:

public class UserGroupConfig : IEntityTypeConfiguration<UserGroup>
{
    public void Configure(EntityTypeBuilder<UserGroup> builder)
    {
         builder.HasKey(t => new { t.UserID, t.GroupID }).HasName("pk_user_group");

         builder.ToTable("tb_user_group");

         builder.Property(t => t.UserID)
                .HasColumnName("UserID")
                .ValueGeneratedOnAdd()
                .IsRequired()
                .HasColumnType("int");

         builder.Property(t => t.GroupID)
                .HasColumnName("GroupID")
                .ValueGeneratedOnAdd()
                .IsRequired()
                .HasColumnType("int");

         builder.HasOne(t => t.User)
                .WithMany(t => t.UserGroup)
                .HasForeignKey(t => t.UserID)
                .HasConstraintName("fk_user_group_user");

         builder.HasOne(t => t.Group)
                .WithMany(t => t.UserGroup)
                .HasForeignKey(t => t.GroupID)
                .HasConstraintName("fk_group_group_user");
    }
}
Enter fullscreen mode Exit fullscreen mode

Abaixo fazemos o mapeamento da entidade de junção para a tabela do banco de dados, e conseguimos definir os nomes.

         builder.HasOne(t => t.User)
                .WithMany(t => t.UserGroup)
                .HasForeignKey(t => t.UserID)
                .HasConstraintName("fk_user_group_user");

         builder.HasOne(t => t.Group)
                .WithMany(t => t.UserGroup)
                .HasForeignKey(t => t.GroupID)
                .HasConstraintName("fk_group_group_user");
Enter fullscreen mode Exit fullscreen mode

No momento de adicionar um novo relacionamento, não é alguma coisa muito bonitinha, você precisa fazer o relacionamento criado uma nova instancia da entidade de junção.

    var user = new User { UserName = "José da Silva" };

    context.User.Add(user); // Adiciona um novo usuário ao Contexto

    var group = new Group { Name = "Administradores" };

    // Relaciona o usuário ao Novo Grupo ***** Com entidade de junção
    group.UserGroup.Add(new UserGroup { UserID = user.UserID });

     // Adiciona um novo grupo ao Contexto
     context.Group.Add(group);

     // Persiste na banco de dados (in memory)
     context.SaveChanges();
Enter fullscreen mode Exit fullscreen mode

E para recuperar também é chatinho, veja:

    foreach (var itemUser in users)
        foreach (var itemGroup in itemUser.UserGroup) // Obtém valores many to many com entidade de relacionamento
            Console.WriteLine(itemUser.UserName + " é membro do grupo: " + itemGroup.Group.Name);

    foreach (var itemGroup in groups)
        foreach (var itemUser in itemGroup.UserGroup) // Obtém valores many to many com entidade de relacionamento
            Console.WriteLine(itemGroup.Name + " tem como membro o usuário: " + itemUser.User.UserName);

Enter fullscreen mode Exit fullscreen mode

EFCore5.Many2Many

Com o EF Core 5, as coisas ficam um pouco mais claras. Usaremos o mesmo exemplo, porém vamos fazer o mapeamento sem precisar criar uma entidade de junção.

public class User
{
    public int UserID { get; set; }
    public string UserName { get; set; }

    public virtual ICollection<Group> Groups { get; set; } = new List<Group>();
}

public class Group
{
    public int GroupID { get; set; }
    public string Name { get; set; }

    public virtual ICollection<User> Users { get; set; } = new List<User>();
}
Enter fullscreen mode Exit fullscreen mode

Observe que agora não existe mais a entidade de junção e o relacionamento é direto entre usuário e grupo.
Neste ponto o mapeamento das entidades para o banco de dados fica bastante simples, e não precisamos de muitas declarações para conseguirmos um resultado melhor.
Neste caso eu fiz o mapeamento na própria classe GroupConfig.

public void Configure(EntityTypeBuilder<Group> builder)
{
    builder.HasKey(t => t.GroupID).HasName("pk_tb_group");

    builder.ToTable("tb_group");

    builder.Property(t => t.GroupID)
           .HasColumnName("GroupID")
           .ValueGeneratedOnAdd()
           .IsRequired()
           .HasColumnType("int");

     builder.Property(t => t.Name)
            .HasColumnName("Name")
            .HasColumnType("varchar")
            .HasMaxLength(100)
            .IsRequired();

    builder.HasMany(t => t.Users)
           .WithMany(t => t.Groups)
           .UsingEntity<Dictionary<string, object>>("UserGroup",
                leftTable => leftTable.HasOne<User>()
                                      .WithMany()
                                      .HasForeignKey("UserID")
                                      .HasConstraintName("fk_user_group_user"),
                rightTable => rightTable.HasOne<Group>()
                                      .WithMany()
                                      .HasForeignKey("GroupID")
                                      .HasConstraintName("fk_group_group_user"),
                j =>
                    {
                        j.HasKey("UserID", "GroupID").HasName("pk_group_user");
                        j.ToTable("tb_group_user");
                    });
}
Enter fullscreen mode Exit fullscreen mode

No EF Core 5 podemos usar o UsingEntity para especificarmos uma junção, e isso é bem simples como o exemplo acima.

Para adicionar a informação ao contexto também é mais direto, claro e fácil, veja:

    var user = new User { UserName = "José da Silva" };

    context.User.Add(user); // Adiciona um novo usuário ao Contexto

    var group = new Group { Name = "Administradores" };

    // Relaciona o usuário ao Novo Grupo ***** Sem entidade de junção
    group.Users.Add(user);

    // Adiciona um novo grupo ao Contexto
    context.Group.Add(group);

    // Persiste na banco de dados (in memory)
    context.SaveChanges();
Enter fullscreen mode Exit fullscreen mode

E a recuperação fica bem mais limpa e fácil.

    foreach (var itemUser in users)
        foreach (var itemGroup in itemUser.Groups) // Obtém valores many to many sem entidade de relacionamento
            Console.WriteLine(itemUser.UserName + " é membro do grupo: " + itemGroup.Name);

    foreach (var itemGroup in groups)
        foreach (var itemUser in itemGroup.Users) // Obtém valores many to many sem entidade de relacionamento
            Console.WriteLine(itemGroup.Name + " tem como membro o usuário: " + itemUser.UserName);
Enter fullscreen mode Exit fullscreen mode

Conclusão

O EF Core 5 é possível eliminar a tabela de junção, que e muito casos tornará a programação mais limpa e prática.

GitHub

Código de Exemplo

Image of Datadog

How to Diagram Your Cloud Architecture

Cloud architecture diagrams provide critical visibility into the resources in your environment and how they’re connected. In our latest eBook, AWS Solution Architects Jason Mimick and James Wenzel walk through best practices on how to build effective and professional diagrams.

Download the Free eBook

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more