EF Core 5.0 - Definindo o relacionamento many-to-many


Hoje vamos mostrar a definição do relacionamento muitos-para-muitos usando o EF Core 5.0.

O EF Core 5.0 é a mais recente versão do EF Core lançada junto com o .NET 5.0 no dia 10 de novembro de 2020 e trás muitas melhorias e novos recursos.

Hoje vou apresentar como definir o relacionamento muitos-para-muitos usando o EF Core 5.0.

Para acompanhar o exemplo você terá que atualizar os seguintes recursos:

  1. .NET Core SDK para a versão 5.0
  2. Visual Studio Community para a versão 16.8.0 (ou superior)

Vou usar também o banco de dados SQL Server 2017 Express.

Criando o projeto Console

Vamos criar um projeto Console(.NET Core) no VS 2019 Community (16.8.1) chamado EFC5_RelMuitosMuitos;

No projeto Console devemos instalar os seguintes pacotes: (estou usando a última versão estável)

Vamos definir um cenário onde temos Usuários e Grupos representados pelas entidades Usuario e Grupo onde podemos ter um relacionamento muitos-para-muitos entre usuários e grupos.

A seguir vamos criar as entidades Usuario e Grupo em uma pasta Models do projeto Console:

1- Entidade Usuario

     public class Usuario
    {
        public int Id { get; set; }
        public string Nome { get; set; }
    }

2- Entidade Grupo

     public class Grupo
    {
        public int Id { get; set; }
        public string Nome { get; set; }
    }

Para poder usar o EF Core 5.0 no projeto vamos criar a classe de contexto AppDbContext em uma pasta Data do projeto:

using EFC5_RelMuitosMuitos.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using static System.Console;
namespace EFC5_RelMuitosMuitos.Data
{
    public class AppDbContext : DbContext
    {
        public DbSet<Usuario> Usuarios { get; set; }
        public DbSet<Grupo> Grupos { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer("..Initial Catalog=EFCore5DB;Integrated Security=True")
            .LogTo(WriteLine, new[] { RelationalEventId.CommandExecuted })
            .EnableSensitiveDataLogging();
    }
}

Neste código definimos o contexto com o EF Core e mapeamos as entidades para as respectivas tabelas que iremos criar no SQL Server.

Definimos o provedor do banco de dados, a string de conexão e estamos usando o novo recurso para exibir no console as consultas geradas pelo EF Core. (veremos esse recurso em outro artigo)

Vamos agora definir o relacionamento muitos para muitos entre Usuario e Grupo no EF Core 5.0.

A primeira coisa a fazer é definir o relacionamento que expressa que um usuário pode estar em muitos grupos e um grupo pode ter muitos usuários. Fazemos isso definindo uma propriedade de coleção usando ICollection<T> em cada entidade:

1- Entidade Usuario

     public class Usuario
    {
        public int Id { get; set; }
        public string Nome { get; set; }
        public ICollection<Grupo> Grupos { get; set; }
    }

2- Entidade Grupo

     public class Grupo
    {
        public int Id { get; set; }
        public string Nome { get; set; }
        public ICollection<Usuario> Usuarios { get; set; }
    }

Neste momento podemos aplicar o Migrations emitindo os comandos na janela Package Manager Console:

add-migration Inicial
update-database

Vamos examinar o script SQL 20201115145518_Inicial.cs gerado na pasta Migrations :

using Microsoft.EntityFrameworkCore.Migrations;
namespace EFC5_RelMuitosMuitos.Migrations
{
    public partial class Inicial : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Grupos",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Nome = table.Column<string>(type: "nvarchar(max)", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Grupos", x => x.Id);
                });
            migrationBuilder.CreateTable(
                name: "Usuarios",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Nome = table.Column<string>(type: "nvarchar(max)", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Usuarios", x => x.Id);
                });
            migrationBuilder.CreateTable(
                name: "GrupoUsuario",
                columns: table => new
                {
                    GruposId = table.Column<int>(type: "int", nullable: false),
                    UsuariosId = table.Column<int>(type: "int", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_GrupoUsuario", x => new { x.GruposId, x.UsuariosId });
                    table.ForeignKey(
                        name: "FK_GrupoUsuario_Grupos_GruposId",
                        column: x => x.GruposId,
                        principalTable: "Grupos",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_GrupoUsuario_Usuarios_UsuariosId",
                        column: x => x.UsuariosId,
                        principalTable: "Usuarios",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });
            migrationBuilder.CreateIndex(
                name: "IX_GrupoUsuario_UsuariosId",
                table: "GrupoUsuario",
                column: "UsuariosId");
        }
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            ...
        }
    }
}

Note que o script de migração vai gerar 3 tabelas:

  1. Usuarios
  2. Grupos
  3. GrupoUsuario

A terceira tabela é a tabela de junção que antes precisávamos definir manualmente e que agora é gerada automaticamente em um relacionamento muitos-para-muitos no EF Core 5.0.

Abrindo o SQL Server Management Studio veremos o banco de dados EFCore5DB criado e as respectivas tabelas com o relacionamento definido:

Recriando o banco e as tabelas e populando com dados via código

Vamos agora recriar o banco de dados e as tabelas e aproveitar para incluir alguns dados de usuários e grupos via código.

Para isso temos que excluir a pasta Migrations do projeto e o banco de dados EFCore5DB do SQL Server.

A seguir no arquivo Program.cs do projeto inclua o código a seguir no método Main:

using EFC5_RelMuitosMuitos.Data;
using EFC5_RelMuitosMuitos.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace EFC5_RelMuitosMuitos
{
    class Program
    {
        static async Task Main(string[] args)
        {
            //disposable assíncrono
            await using var ctx = new AppDbContext();
            //deleta o banco de dados
            await ctx.Database.EnsureDeletedAsync();
            //cria o banco se não existir
            await ctx.Database.EnsureCreatedAsync();
            //define usuarios
            var maria = new Usuario { Nome = "Maria" };
            var manoel = new Usuario { Nome = "Manoel" };
            var jose = new Usuario { Nome = "José" };
            var ana = new Usuario { Nome = "Ana" };
            //define grupos e seus usuarios
            var filmes = new Grupo { Nome = "Filmes", Usuarios = new List<Usuario> { maria, jose, ana } };
            var receitas = new Grupo { Nome = "Receitas", Usuarios = new List<Usuario> { maria, ana } };
            var futebol = new Grupo { Nome = "Futebol", Usuarios = new List<Usuario> { manoel, jose } };
            //inclui os usuários e grupos e persiste
            ctx.AddRange(maria, manoel, jose, ana, filmes, receitas, futebol);
            await ctx.SaveChangesAsync();
            //consulta
            var usuarios = await ctx.Usuarios
                                    .Where(u => u.Grupos.Any(g => g.Nome == "Filmes"))
                                    .ToListAsync();
            //exibe os usuarios do grupo filmes   
            Console.WriteLine($"Grupo - Filmes"); 

            foreach (var usuario in usuarios)
            {
                Console.WriteLine($"{usuario.Nome}");
            }

            Console.ReadLine();
        }
    }
}

Executando este código iremos criar o banco de dados e as tabelas e incluir os dados para usuários e grupos e a seguir estamos realizando uma consulta para retornar os usuários para o grupo 'Filmes' que será exibida no console.

Executando o projeto teremos o seguinte resultado:

No console poderemos ver os comandos SQL gerados pelo EF Core 5 para:

1- criar o banco de dados EFCore5DB

2- criar a tabela Usuarios

3- criar a tabela Grupos

4- criar a tabela GrupoUsuario

5- criar a consulta para exibir os usuários do grupo 'Filmes'

Concluindo :

O EF Core 5.0 agora cria de forma automática a tabela de junção e ao desenvolvedor C# basta definir o relacionamento muitos-para-muitos.

Existem outras considerações que podemos ter neste cenário mas vamos deixar isso para outro artigo.

Na próxima parte do artigo veremos como podemos usar os novos recursos para exibir as consultas geradas e fazer o log.

Pegue o código do projeto aqui: EFC5_RelMuitosMuitos.zip (sem as referências)

"Ai daqueles que nas suas camas intentam a iniqüidade, e maquinam o mal; à luz da alva o praticam, porque está no poder da sua mão!"
Miquéias 2:1


Referências:


José Carlos Macoratti