EF Core - Definindo os relacionamentos - I
Neste artigo veremos como definir os principais relacionamentos entre as entidades usando o EF Core em um projeto prático.


Vamos usar um projeto prático bem simples para mostrar como definir o relacionamento entre entidades usando o EF Core.

 



Assim vamos criar um projeto para gerenciar informações sobre filmes, produtoras, distribuição, orçamento e outros detalhes.

 

Para isso iremos criar um projeto ASP.NET Core MVC que não vai exigir muito conhecimento do Entity Framework, e neste projetos vamos aprender a definir os relacionamentos um para muitos, um para um e muitos para muitos, a gerar o banco de dados e as tabelasa e também como realizar as operações CRUD.

 

Assim iremos realizar as seguintes tarefas durante o desenvolvimento do projeto

1. Criar registros em uma ou várias tabelas de banco de dados ao mesmo tempo.

2. Ler registros únicos, múltiplos e relacionados de tabelas de banco de dados.
3. Atualizar as entradas de registros em uma ou várias tabelas de banco de dados ao mesmo tempo.
4. Excluir entradas de registros de uma ou várias tabelas ao mesmo tempo.

 

Para focar nas funcionalidades do EF Core vamos criar um projeto monolítico como apenas uma camada, a camada de apresentação representada por uma aplicação ASP .NET Core MVC onde no projeto vamos criar pastas para separar responsabilidades na medida do possível. Em uma proxima etapa podemos reestrutura o projeto usando uma arquitetura mais robusta.

 

Dessa forma nesta primeira abordagem um iniciante poderá acompanhar e entender o projeto de forma mais clara.
 

Os recursos usados neste projeto serão os seguintes:

Criando o projeto no VS 2022

Vamos criar um novo projeto usando o template ASP.NET Core Web App (Model-View-Controller) com o nome XFilmes usando as configurações padrão conforme mostrado abaixo:

 


A estrutura do projeto criado é mostrada a seguir:
 


Vamos incluir as referências a dois pacotes do Entity Framework Core:

Podemos usar o menu Tools-> Nuget Package Manager -> Manage Nuget Packages for Solution e acionar a guia Browse e selecionar o pacote , os projetos e clicar no botão Install:



Criando as entidades do modelo de domínio

 

Vamos criar na pasta Models do projeto as classes que representam as nossas entidades do domínio. Vamos criar as seguintes classes:

Nota: As classes que representam as entidades são definidas no singular e as respectivas tabelas serão definidas com o mesmo nome no plural.

 

1- Produtora - Gerencia as informações das empresas responsáveis pela produção do filme

 

using System.ComponentModel.DataAnnotations;
namespace XFilmes.Models;

public class Produtora

{

   public int ProdutoraId { get; set; }

  
   [Required(ErrorMessage=
"Informe o nome")]

   public string? Nome { get; set; }

  
   public
string? Logo { get; set; }
 

   [DisplayFormat(DataFormatString = "{0:n2}", ApplyFormatInEditMode = true)]

   [Display(Name ="Receita Anual")]

   [Required(ErrorMessage = "Informe a receita anual")]

   [Range(100000,999999999)]

   public decimal ReceitaAnual { get; set; } = 0.00M;


   [Display(Name =
"Data de Fundação")]

   [Required(ErrorMessage = "Informe a data de fundação")]

   public DateTime DataCriacao { get; set; } = DateTime.Now;


 
 //relacionamento um-para-muitos com Filme

   public ICollection<Filme> Filmes { get; set; } = new List<Filme>();

   //relacionamento um-para-um com Matriz

   public Matriz? Matriz { get; set; }

}

A classe Produtora define 5 propriedades e a propriedade de navegação de coleção Filmes que será usada para criar um relacionamento um-para-muitos entre as tabelas Produtoras e Filmes no banco de dados após aplicarmos o Migrations.

2- Filme -  Representa as informações de um filme
 

public class Filme

{

   public int FilmeId { get; set; }

   public string? Nome { get; set; }

   public string? Poster { get; set; }
 

   [DisplayFormat(DataFormatString = "{0:n2}", ApplyFormatInEditMode = true)]

   [Display(Name = "Orçamento")]

   public decimal Orcamento { get; set; }
 

   [DisplayFormat(DataFormatString = "{0:n2}", ApplyFormatInEditMode = true)]

   public decimal Faturamento { get; set; }
 

   [Display(Name = "Data de Lançamento")]

   public DateTime DataLancamento { get; set; }

   // one-to-Many com Produtora

   public int ProdutoraId { get; set; }

   public Produtora? Produtora { get; set; }
   // Many-to-Many com FilmeDistribuidora

   public IList<FilmeDistribuidora> FilmesDistribuidoras { get; set; } = new List<FilmeDistribuidora>();

}


Neste código temos que:

 

- ProdutoraId será a chave estrangeira para esta tabela (ou seja, o valor pai será a coluna Id da tabela Produtora).
- Uma propriedade de navegação de referência – Produtora

- Uma propriedade de navegação de coleção chamada FilmesDistribuidoras

A propriedade Produtora é do tipo Produtora e será usada para criar um relacionamento um para muitos com a classe de entidade Produtora.

A propriedade FilmesDistribuidoras é do tipo IList e será usada para criar relacionamentos muitos-para-muitos com a classe de entidade Distribuidora. Para fazer isso, ele usa outra entidade chamada FilmeDistribuidora.

 

3- Matriz - Gerencia os endereços das sedes das produtoras de um filme

 

public class Matriz

{

   public int MatrizId { get; set; }

   public string? Pais { get; set; }

   public string? Cidade { get; set; }

   public string? Local { get; set; }

   public string? Telefone { get; set; }

   public string? Email { get; set; }

   public int ProdutoraId { get; set; }

   public Produtora? Produtora{ get; set; }

}

 

Nesta entidade temos a propriedade de navegação Produtora que vai criar um relacionamento um-para-um entre esta entidade e a entidade Produtora.

 

4- Distribuidora - Gerencia as informações sobre a distribuição dos filmes

 

using System.ComponentModel.DataAnnotations;

namespace XFilmes.Models;
 

public class Distribuidora

{

   public int DistribuidoraId { get; set; }

   public string? Nome { get; set; }
 

   [Required]

   public string? Local { get; set; }


   [Required]

   public string? Telefone { get; set; }

  
// Many-to-Many with Movie

   public IList<FilmeDistribuidora> FilmesDistribuidoras { get; set; } = new List<FilmeDistribuidora>();

}

 

 

A propriedade de navegação de coleção FilmesDistribuidora é do tipo IList e vai criar um relacionamento muitos-para-muitos entre as tabelas Distribuidoras e Filmes que serão geradas no banco de dados. Para isso será usada a entidade FilmeDistribuidora.

 

4- FilmeDistribuidora - É usada como uma tabela de junção para definir o relacionamento muitos-para-muitos

 

public class FilmeDistribuidora

{

   public int FilmeId { get; set; } //foreign key

   public int DistribuidoraId { get; set; } //foreign key

   public Filme? Filme { get; set; } //Reference navigation

   public Distribuidora? Distribuidora { get; set; } //Reference navigation

}

 

Neste código FilmeId é a chave estrangeira associada à entidade Filme, enquanto DistribuidoraId é a chave estrangeira associada à entidade Distribuidora.

 

As propriedades Filme e Distribuidora são propriedades de navegação de referência para criação do relacionamento muitos-para-muitos.

 

Da forma como definimos as entidades estamos usando as convenções do EF Core para definir os relacionamentos entre as tabelas que irão ser geradas no banco de dados. Para isso precisamos definir o mapeamento usando o EF Core criando a classe de contexto.


Criando a classe de contexto

Vamos criar no projeto a pasta Context e nesta pasta vamos criar a classe AppDbContext que vai herdar da classe DbContext que será usada para definir o relacionamento entre as entidades e as tabelas que desejamos gerar.

 

public class AppDbContext :DbContext

{

    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)

    { }
 

    public DbSet<Produtora>? Produtoras { get; set; }

    public DbSet<Filme>? Filmes { get; set; }

    public DbSet<Matriz>? Sedes { get; set; }

    public DbSet<Distribuidora>? Distribuidoras { get; set; }

    public DbSet<FilmeDistribuidora>? FilmesDistribuidoras { get; set; }

}


O código acima define no construtor AppDbContext a utilização da classe DbcontextOptions que carrega as informações de configuração necessárias para configurar o DbContext, e , isso será feito pela classe base.

 

Nota: DbContextOptions também pode ser configurado usando o método OnConfiguring. Este método obtém o DbContextOptionsBuilder como seu argumento. Em seguida, é usado para criar o dbContextOptions.

Da forma que esta definida esta classe não reflete os relacionamentos entre as entidades pois usando as convenções o EF Core não vai conseguir fazer o trabalho sozinho.

 

Teremos então que usar o método OnModelCreating e definir explicitamente as propriedades e os relacionamentos entre as entidades usando a Fluent API.
 

using Microsoft.EntityFrameworkCore;

using XFilmes.Models;

namespace XFilmes.Context;
 

public class AppDbContext :DbContext

{

   public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)

   { }

   public DbSet<Produtora>? Produtoras { get; set; }

   public DbSet<Filme>? Filmes { get; set; }

   public DbSet<FilmeDetalhe>? FilmesDetalhes { get; set; }

   public DbSet<Distribuidora>? Distribuidoras { get; set; }

   public DbSet<FilmeDistribuidora>? FilmesDistribuidoras { get; set; }


   protected
override void OnModelCreating(ModelBuilder modelBuilder)

   {

// Produtora

modelBuilder.Entity<Produtora>().HasKey(s => s.ProdutoraId);
// Produtora<->Matriz

modelBuilder.Entity<Produtora>()

.HasOne<Matriz>(p => p.Matriz)

.WithOne(m => m.Produtora)

.HasForeignKey<Matriz>(m => m.ProdutoraId);

 

modelBuilder.Entity<Produtora>(entity =>

{

entity.Property(e => e.Nome)

.IsRequired()

.HasMaxLength(80);

entity.Property(e => e.Logo)

.HasMaxLength(100);

entity.Property(e => e.ReceitaAnual)

.IsRequired()

.HasColumnType("decimal(12,2)");

entity.Property(e => e.DataCriacao)

.IsRequired()

.HasColumnType("Date");

});
 

// Filme

modelBuilder.Entity<Filme>().HasKey(s => s.FilmeId);

modelBuilder.Entity<Filme>()

   .HasOne(e => e.Produtora)

   .WithMany(e => e.Filmes)

   .HasForeignKey(e => e.ProdutoraId)

.OnDelete(DeleteBehavior.Cascade);
 

modelBuilder.Entity<Filme>(entity =>

{

entity.Property(e => e.Nome)

.IsRequired()

.HasMaxLength(80);

entity.Property(e => e.Poster)

.IsRequired()

.HasMaxLength(100);

entity.Property(e => e.Orcamento)

.IsRequired()

.HasColumnType("decimal(12,2)");

entity.Property(e => e.Faturamento)

.IsRequired()

.HasColumnType("decimal(12,2)");

entity.Property(e => e.DataLancamento)

.IsRequired()

.HasColumnType("Date");

});


// Distribuidora

modelBuilder.Entity<Distribuidora>().HasKey(s => s.DistribuidoraId);

modelBuilder.Entity<Distribuidora>(entity =>

{

entity.Property(e => e.Nome)

.IsRequired()

.HasMaxLength(80);

entity.Property(e => e.Local)

.IsRequired()

.HasMaxLength(100);

entity.Property(e => e.Telefone)

.IsRequired()

.HasMaxLength(25);

});


// FilmeDistribuidora

modelBuilder.Entity<FilmeDistribuidora>().HasKey(t => new { t.FilmeId, t.DistribuidoraId });

modelBuilder.Entity<FilmeDistribuidora>()

   .HasOne(t => t.Filme)

   .WithMany(t => t.FilmesDistribuidoras)

   .HasForeignKey(t => t.FilmeId);

modelBuilder.Entity<FilmeDistribuidora>()

   .HasOne(t => t.Distribuidora)

   .WithMany(t => t.FilmesDistribuidoras)

   .HasForeignKey(t => t.DistribuidoraId);
 

}

}


Vamos entender o código acima:

 

No método OnModelCreating, usamos as APIs Fluent do Entity Framework Core para configurar o modelo do EF para mapeamentos de banco de dados, chave primária, chave estrangeira e relacionamentos.

Para a entidade do Filme, configuramos a sua chave primária como: 


modelBuilder.Entity<Filme>().HasKey(s => s.FilmeId);

No método OnDelete() definimos a configuração DeleteBehavior para Cascade. Isso significa que quando a entidade pai, ou seja, aqui o registro da tabela de banco de dados 'Produtora' for excluído, todos os registros filho na tabela Filme também são excluídos.

 

Outra coisa a notar é a configuração feita para a entidade FilmeDistribuidora onde criamos uma chave primária composta para seus dois campos que são FilmeId e DistribuidoraId como:

 

// FilmeDistribuidora

modelBuilder.Entity<FilmeDistribuidora>().HasKey(t => new { t.FilmeId, t.DistribuidoraId });

modelBuilder.Entity<FilmeDistribuidora>()

   .HasOne(t => t.Filme)

   .WithMany(t => t.FilmesDistribuidoras)

   .HasForeignKey(t => t.FilmeId);

modelBuilder.Entity<FilmeDistribuidora>()

   .HasOne(t => t.Distribuidora)

   .WithMany(t => t.FilmesDistribuidoras)

   .HasForeignKey(t => t.DistribuidoraId);
 

Precisamos concluir esta etapa registro o serviço do contexto na classe Program :

 

using Microsoft.EntityFrameworkCore;

using XFilmes.Context;
 

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllersWithViews();


   var
connection = builder.Configuration.GetConnectionString("DefaultConnection");


   builder.Services.AddDbContext<AppDbContext>(options =>

                   options.UseSqlServer(connection));

   ...

}

 

E finalmente vamos incluir a string de conexão do SQL Server no arquivo appsettings.json :

 

{

"ConnectionStrings": {

"DefaultConnection": "Data Source=\\SQLEXPRESS;Initial Catalog=FilmesDB;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"

},

"Logging": {

"LogLevel": {

"Default": "Information",

"Microsoft.AspNetCore": "Warning"

}

},

"AllowedHosts": "*"

}

 

 

Com isso temos todos os elementos configurados no projeto e podemos aplicar o Migrations.

 

Vamos usar a ferramenta de linha de comando (CLI) do EF Core dotnet ef.

 

Para instalar esta ferramenta no seu ambiente o comando usado é : dotnet tool install --global dotnet-ef

 

Com a ferramenta instalada vamos abrir uma janela do Package Manager Console e emigir o comando:

dotnet ef migrations add Inicial

 

 

Este comando vai criar a pasta Migrations no projeto e nesta pasta vai criar o arquivo de script contendo os comandos para criar o banco de dados e as tabelas.

 

Para executar este arquivo e aplicar os comandos vamos emitir o comando: dotnet ef database update

 

Após o processamento teremos o banco de dados FilmesDB e as tabelas criadas com os relacionamentos definidos como mostra a figura abaixo:

 

 

Na próxima parte do artigo vamos continuar....
 

"Jesus dizia, pois, aos judeus que criam nele: Se vós permanecerdes na minha palavra, verdadeiramente sereis meus discípulos;"
João 8:31

Porque um menino nos nasceu, um filho se nos deu, e o principado está sobre os seus ombros, e se chamará o seu nome: Maravilhoso, Conselheiro, Deus Forte, Pai da Eternidade, Príncipe da Paz.

Isaías 9:6
Porque um menino nos nasceu, um filho se nos deu, e o principado está sobre os seus ombros, e se chamará o seu nome: Maravilhoso, Conselheiro, Deus Forte, Pai da Eternidade, Príncipe da Paz.

Isaías 9:6

Referências:


José Carlos Macoratti