EF Core - Erros a evitar para ter um melhor desempenho
 Hoje vamos recordar alguns procedimentos básicos que se aplicados ao usar o EF Core vão nos garantir um melhor desempenho.


Melhorar o desempenho é sempre um requisito importante a considerar quando usamos uma tecnologia e isso se aplica ao Entity FrameworkCore. Veremos a seguir de forma resumida recomendações de como obter um melhor desempenho usando esta tecnologia.
 

 


 

Um conceito muito importante ao usar o EF Core é saber como funciona o rastreamento das entidades e consultas sem rastreamento.

 

O comportamento de rastreamento controla se o Entity Framework Core manterá informações sobre uma instância de entidade em seu rastreador de alterações.

 

Se uma entidade for rastreada, quaisquer alterações detectadas na entidade persistirão no banco de dados durante o uso de SaveChanges(). O EF Core também corrigirá as propriedades de navegação entre as entidades em um resultado de consulta de rastreamento e as entidades que estão no rastreador de alterações.

 

Por padrão, as consultas que retornam tipos de entidade são rastreadas. O que significa que você pode fazer alterações nessas instâncias de entidade e fazer com que essas alterações persistam usando SaveChanges().

 

Não rastrear as consultas podem ser útil quando os resultados são usados em um cenário somente leitura. Neste contexto as consultas são mais rápidas de executar porque não há necessidade de configurar as informações de controle de alterações.

 

Se você não precisar atualizar as entidades recuperadas do banco de dados, uma consulta sem rastreamento deve ser usada (AsNoTracking). Você pode trocar uma consulta individual para não ser rastreada. Nenhuma consulta de rastreamento também fornecerá resultados com base no que está no banco de dados, desconsiderando quaisquer alterações locais ou entidades adicionadas.

 

Com isso em mente veremos a seguir de forma resumida recomendações com o objetivo de obter um melhor desempenho ao usarmos o EF Core em nossas aplicações.

 

1. Obtenha apenas as colunas que você realmente precisa

Em um contexto onde precisamos obter apenas uma coluna que contém a informação que precisamos retornar todos os dados é um desperdício de recursos.
 

1- Quero obter apenas o Id  do Produto

 

var produto = _context.Produtos.FirstOrDefault(p => p.ProdutoId == id);

return produto.ProdutoId;

 

Porque selecionar o retorno de todas as informações para depois retornar apenasa o Id ?

 

2- Retornando apenas o Id

 

int produtoId = _context.Produtos.Where(p => p.ProdutoId == id)
                        .Select(s => s.ProdutoId).FirstOrDefault();

return produtoId;

 

Mais eficiente.

 

2. Obtenha apenas as linhas que você realmente precisa

 

Em consultas com contagem usando IEnumerable em segundo plano que não notamos acarreta o problema, pois estamos carregando todas as linhas e todas as linhas são rastreadas, e então fizemos a contagem nos dados da memória.

 

Exemplo:  As consultas a seguir são equivalentes :

 


return _context.Produtos.ToList().Count;
 

IEnumerable<Produto> produtos = _context.Produtos;
return produtos.Count();

 

A seguir resolvemos o problema, não carregando todos os dados, e ainda não temos nenhum rastreamento pois estamos retornando apenas um único valor:

...
return   GetProdutos().Count();

...

private IQueryable<Produto> GetProdutos()
{
   return _context.Produtos;
}

 

 

Lembre-se:
 

  • IEnumerable é a melhor escolha para tratar coleção de dados na memória como List, Array, etc.;
  • IEnumerable pode mover somente para frente;
  • IEnumerable não suporta lazy loading, logo não é aconselhável para paginação de dados;
  • IQueryable é a melhor escolha para tratar coleções de dados que não estejam na memória como banco de dados remotos, serviços. etc.;
  • IQUeryable suporta lazy loading
  •  

    3. Desabilite o rastreamento das entidades quando possível

    Quando você chama o método SaveChanges no DbContext, o contexto precisa ser capaz de gerar os comandos apropriados para cada objeto que está atualmente rastreando.

    Assim ele precisa saber sobre o "estado" de cada objeto - seja novo ou seja um objeto existente que tenha sido modificado de alguma forma ou se esta agendado para exclusão. O Change Tracker é o mecanismo responsável por este processo.

    O Change Tracker registra o estado atual de uma entidade usando um dos quatro valores:

    O rastreamento de alterações começa assim que uma entidade é carregada. Uma entidade é carregada como resultado de um retorno de uma consulta ou por estar sendo introduzida no contexto através de um dos seguintes métodos do DbContext : Add, Attach, Update e Remove, ou por ter sua propriedade de State definida na entidade de entrada retornada pela chamada do método de Entry do contexto.

    Assim, o DbContext no Entity Framework é responsável pelo rastreamento das alterações feitas na entidade ou no objeto, de modo que a atualização correta é feita no banco de dados quando o método SaveChange() do contexto é chamado.

    Quando recuperamos entidades usando uma consulta de objeto, o Entity Framework coloca essas entidades em um cache e rastreia quaisquer alterações feitas nessas entidades até que o método SaveChanges seja chamado, então Entity Framework rastreia os resultados da consulta que retornam os tipos de entidade.

    Para desabilitar o rastreamento das consultas e assim otimizar o desempenho podemos :

    Exemplo:

    var produto = _context.Produtos
                           .AsNoTracking()
                           .FirstOrDefault(p => p.ProdutoId == id);

    Podemos também desabilitar o rastreamento a nível de instância de contexto.

    Para isso usamos a propriedade QueryTrackingBehavior da classe ChangeTracker (definida como uma propriedade da classe de contexto).

    _context.ChangeTracker.QueryTrackingBehavior =
                        QueryTrackingBehavior.NoTracking;

    var produto = _context.Produtos
                           .FirstOrDefault(p => p.ProdutoId == id);
     

    No código acima estamos desabilitando o rastreamento definindo a propriedade QueryTackingBehavior da classe ChangeTracker do contexto usando a enumeração QueryTrackingBehavior que indica como os resultados serão rastreados pelo ChangeTracker.

    Os valores possíveis da enumeração QueryTackingBehavior são:

    1. NoTracking - desabilita o rastreamento das entidades retornadas pelas consultas LINQ;
    2. TrackAll - habilita o rastramento para todas as entidades que são retornadas pelas consultas LINQ

    3. Evite usar Joins explicitos

    Quando usamos o método Include para carregar dados relacionados de outras entidades de forma explicita conforme mostra o código a seguir:

     


    var produtos = _context.Produtos
                               .AsNoTracking()
                               .Include(c => c.Categoria)
                               .Where(p => p.ProdutoId == 1);   

     


    Mesmo se não precisamos dos dados relacionados eles sempre serão carregados.

     

    Assim ao invés de usar o include de forma explicita podemos usar uma instrução Select para obter apenas o que desejamos :

     


     var produtoCategoria = _context.Produtos
                                            .AsNoTracking()
                                            .Where(p => p.ProdutoId == 1)
                                            .Select(x => new ProdutoCategoria
                                             {
                                                ProdutoId = x.ProdutoId,
                                                ProdutoNome = x.Nome,
                                                ProdutoPreco = x.Preco,
                                                CategoriaId = x.Categoria.CategoriaId,
                                                CategoriaNome = x.Categoria.Nome
                                            });

     

     

    Mais eficiente.

     

    4. Reutilize o Contexto quando possível

    A idéia é simples : reutilizar o contexto e assim abrir menos conexões com o banco de dados.

     

    O DbContext não é thread-safe. Portanto, você não pode reutilizar o mesmo objeto DbContext para várias consultas ao mesmo tempo (coisas estranhas acontecem). A solução usual para isso tem sido apenas criar um novo objeto DbContext toda vez que você precisar de um. Isso é o que AddDbContext faz.

    No entanto, não há nada de errado em reutilizar um objeto DbContext após a conclusão de uma consulta anterior. Isso é o que AddDbContextPool faz. Ele mantém vários objetos DbContext ativos e fornece um não utilizado em vez de criar um novo a cada vez.

    Qual você usa fica a seu critério. Ambos funcionarão. O pooling tem alguns ganhos de desempenho. No entanto, a documentação avisa que, se você usar alguma propriedade privada em sua classe DbContext que não deve ser compartilhada entre consultas, você não deve usá-la. Imagino que isso seja muito raro, portanto, o pooling deve ser apropriado na maioria dos casos.


    Exemplo de uso:
     

    
    builder.Services.AddDbContextPool<AppDbContext>(options =>
                        options.UseMySql(mySqlConnection,
                        ServerVersion.AutoDetect(mySqlConnection)));

    Antes de fazer a configuração, verifique a documentação da Microsoft para o tamanho do pool padrão para EF CORE, ele pode ser diferente para diferentes versões do EF CORE.

    5 - Utilize consultas compiladas sempre que necessário

    Quando um aplicativo executa muitas vezes consultas estruturalmente similares no Entity Framework, geralmente é possível melhorar o desempenho compilando a consulta uma única vez e executando-a várias vezes com parâmetros diferentes.

    A classe CompiledQuery fornece a compilação e o cache de consultas para reutilização. Conceitualmente, essa classe contém um método CompiledQuery de Compile com várias sobrecargas. Chame o método Compile para criar um novo delegado para representar a consulta compilada.

    Os métodos Compile, com ObjectContext e valores de parâmetro, retornam um delegado que gera um resultado (como uma instância IQueryable<T>). A consulta é compilada somente uma vez durante a primeira execução. As opções de mesclagem definidas para a consulta no momento da compilação não podem ser alteradas posteriormente. Uma vez compilada a consulta, você só pode fornecer parâmetros de tipo primitivo, mas não pode substituir partes da consulta que alterariam o SQL gerado.

    Assim é uma boa prática realizar consultas usando consultas compiladas se a consulta for freqüentemente usada para buscar os registros do banco de dados.A consulta será lenta na primeira vez, mas depois ela vai melhorar o desempenho de forma significativa. Para compilar uma consulta podemos usar o método Compile da classe CompiledQuery.

    Assim para recuperar os detalhes dos clientes várias vezes baseado em um critério como a cidade podemos fazer essa consulta compilada da seguinte forma:

    - Consulta simples (não compilada)

    IQueryable clientes = from cliente in _context.Clientes
                                       where cliente.Cidade == "Lins"
                                       select cliente;
     

    - Consulta Compilada

    static readonly Func<AdventureWorksEntities, Decimal> s_compiledQuery3MQ =
                                      CompiledQuery.Compile<AdventureWorksEntities, Decimal>(
                                      ctx => ctx.Products.Average(product => product.ListPrice));
    static void CompiledQuery3_MQ()
    {
        using (AdventureWorksEntities context = new AdventureWorksEntities())
        {
            Decimal averageProductPrice = s_compiledQuery3MQ.Invoke(context);
            Console.WriteLine("O Preco medio dos produtos é $: {0}", averageProductPrice);
        }
    }

    O exemplo a seguir compila e invoca uma consulta que retorna a média dos preços na lista de produtos como um valor Decimal.

    Regras de uso para consultas compiladas :

    1. Crie um delegado Func estático que aceitará DbContext e retornará IEnumerable;

    2. Você não pode retornar IQuerable de consultas compiladas;

    3. As consultas compiladas só podem ser usadas em um único modelo do EF Core. Às vezes, diferentes instâncias de contexto do mesmo tipo podem ser configuradas para usar modelos diferentes; a execução de consultas compiladas neste cenário não é suportada.

    4. Ao usar parâmetros em consultas compiladas, use parâmetros escalares simples. Expressões de parâmetros mais complexas — como acessos de membros/métodos em instâncias — não são suportadas.

    E estamos conversados... 

     

    "Porque todos sois filhos de Deus pela fé em Cristo Jesus.Porque todos quantos fostes batizados em Cristo já vos revestistes de Cristo."
    Gálatas 3:26,27


    Referências:


    José Carlos Macoratti