Alura > Cursos de Programação > Cursos de .NET > Conteúdos de .NET > Primeiras aulas do curso .NET: avance na persistência com EF Core

.NET: avance na persistência com EF Core

Melhorando consultas - Apresentação

Olá! Te desejo as boas-vindas ao curso da Formação em Integração de Banco de Dados utilizando EF-Core. Meu nome é André Bessa e serei seu instrutor.

Audiodescrição: André se descreve como um homem negro, com barba por fazer, cabelo baixo e rosto arredondado. Veste uma camisa vermelha e o ambiente ao seu redor é iluminado com tons de azul. Ao fundo, uma estante com itens decorativos.

Antes de começarmos, é essencial que você tenha concluído o curso anterior, além de ter conhecimento em orientação a objetos, linguagem C# e bancos de dados.

Este curso é direcionado para quem já finalizou o curso anterior e deseja aprimorar ainda mais suas habilidades em integração de bases de dados utilizando Entity Framework Core, além de evoluir o sistema com boas práticas e padrões de projeto.

Ao longo do curso, vamos explorar como otimizar consultas com EF-Core, enriquecer nosso modelo de domínio utilizando Owned Entities e Value Converters. Também vamos aprofundar no uso de transações, adotando boas práticas como o Unit of Work e o padrão Repository. Além disso, aprenderemos a interceptar comandos que serão executados na base de dados.

Continuaremos o desenvolvimento do projeto Freelando, que você já conhece do curso anterior, melhorando desde seu banco de dados legado até a concepção geral do sistema.

Aproveite ao máximo os recursos da plataforma! Além dos vídeos e atividades, temos um fórum de dúvidas e uma comunidade ativa no Discord. Então, vamos começar?

Melhorando consultas - Otimizando consultas

Olá, pessoal!

Vamos dar continuidade à manutenção do projeto Freelando, que permite cadastrar e gerenciar projetos freelancer. Nosso foco é manter o processo de melhoria contínua, considerando que o banco de dados do Freelando é legado. Isso significa que seguimos um padrão definido anteriormente pela equipe de projetos, mas agora estamos integrando novos recursos com o Entity Framework.

Performance em consultas

No curso anterior, discutimos a importância da performance em consultas e vamos seguir a partir desse ponto. Um aspecto crucial das consultas é determinar se realmente precisamos de todas as informações de uma tabela. Por exemplo, ao abrir o endpoint de clientes, notamos que ele retorna todas as informações disponíveis na tabela, mas será que realmente precisamos de tudo?

Imagine um componente de front-end do tipo Select que exibe apenas o nome do cliente. Nesse caso, trazer dados como endereço não faz sentido, além de impactar negativamente a performance.

Criando um novo endpoint

Vamos otimizar esse endpoint de clientes para retornar apenas as informações necessárias. Para isso, selecionamos e copiamos o trecho de código. Depois, o colamos logo abaixo para editá-lo.

Trecho de código a ser duplicado:

app.MapGet("/clientes", async ([FromServices] ClienteConverter converter, [FromServices] FreelandoContext contexto) =>
{
    var clientes = converter.EntityListToResponseList(contexto.Clientes.ToList());
    return Results.Ok(await Task.FromResult(clientes));
})
.WithTags("Cliente")
.WithOpenApi();

No caso dos clientes, queremos retornar apenas o ID e o nome. Para isso, criaremos um novo endpoint chamado identificador-nome, que nos permitirá comparar os resultados.

Vamos demonstrar como trabalhar com converters para aplicar essa funcionalidade. Primeiro, apagaremos o conteúdo da linha var clientes = converter.EntityListToResponseList(contexto.Clientes.ToList()) e substituiremos pelo seguinte trecho:

var clientes = contexto.Clientes.Select(c => new { Identificador = c.Id, Nome = c.Nome });

Nele, estamos criando uma lista de clientes utilizando o método .Select(), do Linq, que permite selecionar apenas os campos desejados. Nesse caso, vamos selecionar o ID e o nome dos clientes. O objeto contexto, que representa nosso banco de dados, será utilizado para acessar a tabela de clientes e criar um novo objeto contendo apenas essas informações essenciais de identificador e nome do cliente.

O código ficará assim:

app.MapGet("/clientes", async ([FromServices] ClienteConverter converter, [FromServices] FreelandoContext contexto) =>
{
    var clientes = converter.EntityListToResponseList(contexto.Clientes.ToList());
    return Results.Ok(await Task.FromResult(clientes));
})
.WithTags("Cliente")
.WithOpenApi();

app.MapGet("/clientes/identificador-nome", async ([FromServices] ClienteConverter converter, [FromServices] FreelandoContext contexto) =>
{
    var clientes = contexto.Clientes.Select(c => new { Identificador = c.Id, Nome = c.Nome });
    return Results.Ok(await Task.FromResult(clientes));
})
.WithTags("Cliente")
.WithOpenApi();

Teste e validação do novo endpoint

Depois de salvar, teremos um endpoint personalizado. Ao executar o projeto, o Swagger provavelmente será aberto em outra tela. No Swagger, navegaremos até a seção de clientes e selecionaremos o endpoint /clientes/identificador-nome. Clicamos neste endpoint, depois em "Try it out", e executamos a consulta.

Como esperado, o resultado exibido contém apenas o identificador e o nome dos clientes, confirmando que a consulta personalizada está funcionando corretamente.

Essa abordagem otimiza o uso do banco de dados, já que, em vez de trazer todas as colunas da tabela, que podem variar dependendo da complexidade e dos relacionamentos, obtemos apenas um conjunto reduzido de informações.

Utilizando o Linq para criar objetos anônimos

Retornando ao endpoint identificador-nome, utilizamos o operador Linq para criar um objeto anônimo, que não possui nome ou tipo definido, mas contém propriedades específicas. O compilador do .NET gera esse objeto, que é posteriormente convertido em JSON e exibido pela API.

Essa prática é crucial para otimizar nossas consultas, retornando apenas os dados necessários e, consequentemente, melhorando o desempenho geral da aplicação.

Próximos passos

Na sequência, vamos continuar aprofundando o tema de performance em consultas.

Melhorando consultas - Consultas divididas

Olá, pessoal!

Vamos continuar com a manutenção do nosso sistema, o Freelando, que utiliza uma base de dados legado. Estamos focados em melhorias e ajustes, especialmente na performance das consultas.

No vídeo anterior, abordamos como otimizar consultas trazendo apenas os dados essenciais utilizando a função .Select() do Linq para criar objetos anônimos, o que facilita o retorno de dados apenas necessários para o front-end e para o Swagger.

Trabalhando com dados relacionados

Agora, vamos nos concentrar em um aspecto importante das consultas: o trabalho com dados relacionados. Por exemplo, queremos obter informações das tabelas de cliente, projeto e especialidade. Para isso, montaremos uma consulta SELECT que pode incluir INNER JOIN, LEFT JOIN, OUTER JOIN, ou até mesmo subselect. Em suma, uma consulta mais complexa.

Criando um novo endpoint

Vamos analisar esse cenário utilizando a tabela de clientes. Criaremos um novo endpoint chamado cliente/projeto-especialidade, com o objetivo de trazer informações de clientes, seus projetos e especialidades.

app.MapGet("/clientes/projeto-especialidade", async ([FromServices] ClienteConverter converter, [FromServices] FreelandoContext contexto) =>
        {
            var clientes = contexto.Clientes.Include(x => x.Projetos).ThenInclude(p => p.Especialidades).ToList();

            return Results.Ok(await Task.FromResult(clientes));
        }).WithTags("Cliente").WithOpenApi();

Se o seu projeto estiver com o AsNoTracking habilitado, você conseguirá trazer os dados relacionados na consulta. Aqui, utilizaremos as funções Include() e ThenInclude() para realizar essas junções, que serão convertidas em inner joins ou left joins, dependendo da necessidade, para buscar os dados nas tabelas.

Teste e validação

Depois de configurar essa consulta, vamos salvar e executá-la para verificar os resultados.

Ao abrir o Swagger e executar o novo endpoint cliente/projeto-especialidade, podemos observar que a consulta funciona conforme esperado. Os dados dos projetos e especialidades relacionados aos clientes estão sendo retornados corretamente.

Ao analisar o resultado no console, observamos que ele gerou um único SELECT, um comando bastante complexo, onde realiza um LEFT JOIN dentro de um subselect que abrange projetos, clientes e especialidades, seguido por outro LEFT JOIN e finalizado com um INNER JOIN. É uma consulta complexa!

Retorno omitido.

Otimização da consulta: uso de .AsSplitQuery()

Agora, é importante destacar que, ao trabalharmos com essas tabelas, que podem conter um grande volume de registros, esse tipo de Select pode impactar a performance da consulta, dependendo do tamanho da base de dados. Mas há uma forma de melhorar essa performance.

Em cliente/projeto-especialidade, especificamente na linha var clientes, podemos instruir o sistema a pegar essa consulta e dividi-la em duas ou três subconsultas menores. Essas subconsultas podem ser mais eficientes na execução dos comandos no banco de dados. Para isso, antes de .ToList(), usaremos .AsSplitQuery(), uma extensão do Linq.

app.MapGet("/clientes/projeto-especialidade", async ([FromServices] ClienteConverter converter, [FromServices] FreelandoContext contexto) =>
        {
            var clientes = contexto.Clientes.Include(x => x.Projetos).ThenInclude(p => p.Especialidades).AsSplitQuery().ToList();

            return Results.Ok(await Task.FromResult(clientes));
        }).WithTags("Cliente").WithOpenApi();

Comparação de resultados: consulta única vs. dividida

Agora, vamos executar novamente o projeto, abrir o Swagger e também o terminal do servidor.

Ao executar a consulta cliente/projeto-especialidade, note que ela continua funcionando como esperado, retornando os dados relacionados. Contudo, observe o que acontece no terminal: ao invés de uma única consulta complexa, o sistema executou pelo menos três consultas menores.

Retorno omitido.

Primeiro, ele realizou um SELECT em clientes, depois um SELECT com um INNER JOIN nos projetos, e, finalmente, um SELECT mais elaborado. Ao quebrar a consulta dessa forma, conseguimos otimizar a performance do banco de dados, que é exatamente o que buscamos.

Utilizando a função .AsSplitQuery(), dividimos uma consulta única em consultas menores e menos complexas. Essa abordagem impacta positivamente a execução das consultas no banco de dados, algo essencial para o processo de evolução e melhoria contínua do nosso projeto.

É importante lembrar que essa é uma etapa de manutenção do projeto Freelando. Refatorações como essa, assim como a adição de novas funcionalidades, são fundamentais neste processo.

Performance e explosão cartesiana

A divisão das consultas é útil porque, ao realizarmos joins entre tabelas, corremos o risco de enfrentar uma explosão cartesiana. Isso ocorre quando o banco de dados precisa relacionar grandes volumes de informações de diferentes tabelas, o que pode consumir muitos recursos do servidor. Ao quebrar a consulta em partes menores, conseguimos otimizar esse processo.

A Microsoft também aborda essa questão na seção "Consultas únicas vs. consultas divididas" da documentação, onde destaca problemas de desempenho, como a explosão cartesiana, e sugere boas práticas para evitar esses problemas ao utilizar o EF-Core.

Próximos passos

A seguir, continuaremos focados na melhoria de performance das consultas enquanto trabalhamos no Freelando. Espero você no próximo vídeo!

Sobre o curso .NET: avance na persistência com EF Core

O curso .NET: avance na persistência com EF Core possui 88 minutos de vídeos, em um total de 54 atividades. Gostou? Conheça nossos outros cursos de .NET em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda .NET acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas