Many-To-Many

29 minuto(s) de leitura - August 14, 2020

01


Nesse post irei falar sobre um dos recursos mais solicitados do Entity Framework Core, e que estará disponível na versão 5 do EF Core o Many-To-Many, ou, muitos-para-muitos.

FYI: Para o exemplo que será apresentado aqui estou utilizando build noturno e você pode também instalar esses pacotes em seu projeto, assim você sempre terá a ultima compilação do projeto, para testar todas funcionalidades novas que estão sendo implementadas, veja aqui o que você precisa fazer.

EF Core 3.1

Até a versão EF Core 3.1, era necessário criar uma terceira classe para que o ORM conseguisse fazer o mapeamento do modelo de dados corretamente, isso funcionava bem, mas os desenvolvedores não gostaram da ideia de conviver com essa nova abordagem, além de não ser a melhor abordagem para o que realmente é proposto, dado que um dos objetivos ao usar um ORM é que ele fique com essa complexidade, assim ajudando manter um código mais limpo.

Cenário

Vamos pensar em um cenário onde precisamos cadastrar alunos e cursos, logo um aluno poderá ter vários cursos, da mesma forma um curso pode ter vários alunos, esse tipo de cardinalidade é utilizado para o relacionamento entre duas entidades, geralmente você irá ver muitos exemplos assim "N:N", é como abreviamos.

Como funcionava no EF Core 3.1?

Para o cenário que falei logo acima, com o EFCore 3.1 temos as seguintes classes para representar nossas entidades, e montar nosso relacionamento N:N, sendo assim classes abaixo são necessárias para configurar nosso modelo de dados.
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<CourseStudent> CourseStudents { get; } = new List<CourseStudent>();
}

public class Course
{
    public int Id { get; set; }
    public string Description { get; set; }

    public IList<CourseStudent> CourseStudents { get; } = new List<CourseStudent>();
}

public class CourseStudent
{
    public int CourseId { get; set; }
    public Course Course { get; set; }
    public int StudentId { get; set; }
    public Student Student { get; set; }
}

Work around

O problema é que para que esse relacionamento realmente seja interpretado pelo EF Core 3.1 e seja capaz fazer o mapeamento correto do seu modelo de dados, é necessário criar uma terceira classe "CourseStudent", e isso realmente é o que muitos não concordam em fazer, e eu concordo plenamente com isso, pois essa complexidade o Entity Framework Core deveria ser capaz abstrair, também você era forçado a fazer uma configuração explícita usando Fluent API para que o mapeamento correto do seu modelo de dados funcionasse, então já que o Entity Framework Core não era capaz de fazer esse mapeamento de forma mais inteligente, então era necessário fazer algo assim:
public class SampleManyToManyContext : DbContext
{
    public DbSet<Student> Students { get; set; }
    public DbSet<Course> Course { get; set; }

    // Configure Explicit
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<CourseStudent>()
            .HasKey(p => new { p.CourseId, p.StudentId });

        modelBuilder.Entity<CourseStudent>()
            .HasOne(p => p.Student)
            .WithMany(p => p.CourseStudents)
            .HasForeignKey(p => p.StudentId);

        modelBuilder.Entity<CourseStudent>()
            .HasOne(p => p.Course)
            .WithMany(p => p.CourseStudents)
            .HasForeignKey(p => p.CourseId);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer("Data source=(localdb)\\mssqllocaldb;Initial Catalog=SampleManyToMany31;Integrated Security=true");
}
Eu já falei em algumas palestras minhas, que se você apenas expor suas entidades em propriedades DbSet em seu contexto o EF Core irá tentar fazer o mapeamento do seu modelo de dados automaticamente, mas, mesmo que você tente expor sua entidade na propriedade DbSet de seu contexto, já que EF Core é capaz de configurar seu modelo de dados com base nessas propriedades, ele irá tentar e falhará, ele não é capaz de resolver esse mapeamento N:N de forma automática para você, sendo assim necessário fazer a configuração acima, caso contrário você receberá o seguinte erro:
System.InvalidOperationException: 'The entity type 'CourseStudent' requires a primary key to be defined. 
If you intended to use a keyless entity type call 'HasNoKey()'.'

Equipe

A equipe do Entity Framework Core vem fazendo um excelente trabalho, sempre focado na qualidade e melhoria do ORM, para entregar para você uma ferramenta poderosa e performática, então tendo implementado outras diversas features ao produto, chegou a vez do Many-To-Many, com um suporte e mapeamento mais adequado que anteriormente mostrado. Inclusive você pode acompanhar a discussão sobre a feature clicando aqui, essa nova feature está em boas mãos e está sendo desenvolvida e liderada por Andriy Svyryd e Smit Patel.

E agora como ficou?

O suporte N:N basicamente posso dizer que está finalizado, dado que está em fase de Release Candidate, e será lançado oficialmente em novembro, mas como citei no topo desse post, você pode já experimentar usando builds noturnos, sendo assim nossas classes agora ficaram muito mais simples, com base nas classes apresentadas acima, fiz pequenas alterações, observe que para o contexto do que é realmente proposto fica muito mais legível, então nossas classes ficaram assim:
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<Course> Courses { get; } = new List<Course>();
}

public class Course
{
    public int Id { get; set; }
    public string Description { get; set; }

    public IList<Student> Students { get; } = new List<Student>();
}

Student agora tem a lista de Courses e não mais a lista de uma terceira classe, da mesma forma Course agora tem a lista de Students, isso faz muito mais sentido.
Veja também como nosso contexto ficou muito mais simples, basta apenas expor as entitdades em uma propriedade DbSet da seguinte forma:

public class SampleManyToManyContext : DbContext
{
    public DbSet<Student> Students { get; set; }
    public DbSet<Course> Course { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer("Data source=(localdb)\\mssqllocaldb;Initial Catalog=SampleManyToMany5;Integrated Security=true");
}

Ficou muito simples não é?!
O Entity Framework Core agora é capaz de fazer o mapeamento correto apenas expondo nossas entidades em nosso contexto, observe que não precisei configurar nada no exemplo acima, isso porque o Entity Framework Core por conversão já faz todo mapemento pra gente de forma automatizada.

Mepeamento Explícito

Eu sou capaz de fazer essa junção de tabelas explicitamente?
A resposta é sim, e é muito simples de fazer isso, você pode fazer de 2 formas, a primeira é expondo uma propriedade DbSet em seu contexto veja um exemplo, onde eu criei mais uma classe CourseStudent que contém algumas propriedades adicionais como Protocol e CreatedAt:


public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<Course> Courses { get; } = new List<Course>();
}

public class Course
{
    public int Id { get; set; }
    public string Description { get; set; }

    public IList<Student> Students { get; } = new List<Student>();
}

[Keyless]
public class CourseStudent
{
    public int CourseId { get; set; }
    public int StudentId { get; set; }
    public string Protocol { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class SampleManyToManyContext : DbContext
{
    public DbSet<Student> Students { get; set; }
    public DbSet<Course> Courses { get; set; }
    public DbSet<CourseStudent> CourseStudents { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .EnableSensitiveDataLogging() // Show Data
            .LogTo(Console.WriteLine, new[] { RelationalEventId.CommandExecuted })
            .UseSqlServer("Data source=(localdb)\\mssqllocaldb;Initial Catalog=SampleManyToManyExplicit5;Integrated Security=true");
}

A segunda forma de fazer é usando Fluent API você pode fazer o mapeamento explicitamente, fazendo a junção de suas entidades, observe que agora eu tenho um novo método de extensão para fazer isso que é o UsingEntity vejo um exemplo completo:


public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<Course> Courses { get; } = new List<Course>();
}

public class Course
{
    public int Id { get; set; }
    public string Description { get; set; }

    public IList<Student> Students { get; } = new List<Student>();
}

public class CourseStudent
{
    public int CourseId { get; set; }
    public int StudentId { get; set; }
    public string Protocol { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class SampleManyToManyContext : DbContext
{
    public DbSet<Student> Students { get; set; }
    public DbSet<Course> Courses { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Configure Explicit
        modelBuilder
            .Entity<Student>()
            .HasMany(p => p.Courses)
            .WithMany(p => p.Students)
            .UsingEntity<CourseStudent>(
                p => p.HasOne<Course>().WithMany(),
                p => p.HasOne<Student>().WithMany());

        modelBuilder
            .Entity<CourseStudent>(p =>
            {
                p.Property(e => e.Protocol).HasColumnType("VARCHAR(32)");
                p.Property(e => e.CreatedAt).HasDefaultValueSql("GETDATE()");
            });              
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .EnableSensitiveDataLogging() // Show Data
            .LogTo(Console.WriteLine, new[] { RelationalEventId.CommandExecuted })
            .UseSqlServer("Data source=(localdb)\\mssqllocaldb;Initial Catalog=SampleManyToManyExplicit5;Integrated Security=true");
}

static void Main(string[] args)
{
    using var db = new SampleManyToManyContext();
    db.Database.EnsureDeleted();
    db.Database.EnsureCreated();

    var students = db.Students.Include(p => p.Courses).ToList();
    var courses = db.Courses.Include(p => p.Students).ToList();
    var courseStudent = db.Set<CourseStudent>().FirstOrDefault();

    var protocol = courseStudent.Protocol;
    var createdAt = courseStudent.CreatedAt;
}

Many-To-Many está sendo rastreado em:
Issue-10508
Issue-1368

Build noturno clique aqui
Exemplos apresentados aqui

Twitter

Fico por aqui! 😄
Me siga no twitter: @ralmsdeveloper


Deixe um comentário