EF Core - Dicas de desempenho


 Hoje veremos algumas dicas para melhorar o desempenho do EF Core.
 
O Entity Framework é um mapeador objeto-relacional (ORM) que permite trabalhar com bancos de dados usando objetos .Net. Ele fornece uma conexão com o banco de dados e também permite a manipulação de bancos de dados com classes de entidade.

Principais recursos do EF Core

Modelagem: O EF Core permite associar tabelas de banco de dados e objetos de entidade e acessar dados através desses objetos.

Migração: A migração do EF Core permite que os desenvolvedores atualizem incrementalmente o esquema do banco de dados.

Consulta: O Entity Framework Core usa Consulta Integrada à Linguagem (LINQ) para consultar dados do banco de dados. Podemos consultar dados do banco de dados com LINQ como uma consulta SQL.

Mapeamento: O EF Core oferece flexibilidade no mapeamento de objetos de entidade para tabelas de banco de dados. Isto é muito útil quando precisamos trabalhar com um banco de dados que não foi criado com EF Core.

Transação: A transação do EF Core é uma unidade de trabalho que lida com várias operações de banco de dados como um único grupo. Se todas as transações forem bem-sucedidas, confirmamos a transação com o método Commit(). Se tivermos uma exceção, revertemos a transação. O método RollBack() reverte todas as operações.

Concorrência: O EF Core fornece suporte para simultaneidade, o que permite que vários usuários trabalhem com os mesmos dados ao mesmo tempo. O EF usa controle de simultaneidade otimista para detectar e resolver conflitos quando vários usuários tentam atualizar os mesmos dados.

Desempenho

Existem muitos fatores que afetam o desempenho de uma aplicação e  os mais comuns são o número de viagens de ida e volta ao banco de dados, a quantidade de dados recuperados e o custo de transformação desses dados em objetos.

Quando se trata de desempenho, existem várias considerações importantes a serem levadas em conta ao usar o EF Core e veremos a seguir algumas dicas importantes para obter um melhor desempenho usando o EF Core;

1- Usar Paginação de dados   

Não obtenha todos os dados de uma vez.

Obtenha dados de apenas uma página com os métodos Skip() e Take().

Skip() -  pula os dados até o parâmetro fornecido.
Take() -  obtém dados do tamanho do parâmetro fornecido.

Ao usar Skip e Take para implementar a paginação, você pode trazer uma quantidade específica de registros em cada consulta, o que é útil para evitar a carga desnecessária de grandes volumes de dados.

var blogs = context.Blogs
    .OrderBy(b => b.BlogId)
    .Skip(10) // Pula os primeiros 10 registros
    .Take(5)  // Retorna os próximos 5 registros
    .ToList();

Se estiver usando Skip e Take em conjunto com ordenação, certifique-se de que a ordenação ocorra antes de Skip e Take. Isso é importante para garantir resultados consistentes e evitar problemas de desempenho.

var blogs = context.Blogs
    .OrderBy(b => b.BlogId)
    .Skip(10)
    .Take(5)
    .ToList();

Ao usar Skip e Take, especialmente em conjunto com OrderBy, certifique-se de que as colunas envolvidas na ordenação tenham índices apropriados definidos no banco de dados para otimizar o desempenho.

2- Usar AsNoTracking

O método AsNoTracking() é uma funcionalidade do Entity Framework Core que desativa o rastreamento de entidades, o que significa que as entidades recuperadas do banco de dados não são rastreadas por padrão para detecção de alterações. O uso de AsNoTracking() pode ter um impacto significativo no desempenho, especialmente em operações de leitura.

AsNoTracking() é especialmente útil em cenários de leitura, onde não há a intenção de modificar as entidades recuperadas. Isso evita a sobrecarga associada ao rastreamento de alterações.

var blogs = context.Blogs
    .AsNoTracking()
    .ToList();

Desativar o rastreamento pode reduzir significativamente o overhead associado à detecção de alterações e à manutenção do estado das entidades, resultando em consultas mais eficientes.

Além disso sem o rastreamento, as consultas se tornam mais leves e podem executar mais rapidamente, uma vez que não há necessidade de manter um estado de rastreamento para cada entidade recuperada.

Como as entidades não são rastreadas, menos memória é consumida, o que pode ser benéfico ao lidar com grandes conjuntos de dados.

É importante avaliar o impacto específico no seu cenário, pois pode haver casos em que o rastreamento de entidades é necessário. O uso de AsNoTracking() deve ser considerado com base nos requisitos e características específicas da operação que está sendo realizada.

3- Usar a programação assíncrona (async/await)

A programação assíncrona é uma abordagem de paradigma de programação que não bloqueia o thread principal. Proporciona execução simultânea e independente de tarefas sem prejudicar o fluxo de execução do programa. As transações não esperam umas pelas outras e não ocorrem sequencialmente.

O uso de programação assíncrona com async e await no Entity Framework Core pode melhorar significativamente o desempenho em aplicações que precisam lidar com operações de I/O, como consultas a bancos de dados. Aqui estão algumas considerações ao usar programação assíncrona no EF Core:

As operações de banco de dados, como consultas e atualizações, geralmente envolvem operações de I/O. Ao usar programação assíncrona, você pode liberar a thread enquanto a operação de I/O está em andamento, permitindo que a CPU seja utilizada para outras tarefas.

var blogs = await context.Blogs.ToListAsync();

Em aplicações da interface do usuário, como as baseadas em Windows Forms ou WPF, o uso de operações assíncronas evita bloqueios na interface do usuário enquanto aguarda a conclusão de operações demoradas.

private async void Button_Click(object sender, RoutedEventArgs e)
{
    var blogs = await context.Blogs.ToListAsync();
    // Atualizar a interface do usuário com os resultados
}

Ao usar operações assíncronas, você pode iniciar várias operações de forma paralela, aproveitando ao máximo o poder de processamento.

var task1 = context.Blogs.ToListAsync();
var task2 = context.Posts.ToListAsync();

await Task.WhenAll(task1, task2);

var blogs = task1.Result;
var posts = task2.Result;

Lembre-se de que a introdução de operações assíncronas deve ser feita com cuidado, e nem todas as operações se beneficiarão igualmente do uso de async e await. Avalie as necessidades específicas da sua aplicação e escolha a abordagem que melhor atenda aos requisitos de desempenho e escalabilidade.

4- Utilize estratégias de Carregamento (Lasy e Eager Loading)

O desempenho no Entity Framework Core pode ser significativamente impactado pela escolha de estratégias de carregamento, como Lazy Loading e Eager Loading. Vamos abordar cada uma delas:

  1. Lazy Loading:

O Lazy Loading é uma técnica em que os dados relacionados a uma entidade são carregados sob demanda, apenas quando são acessados pela primeira vez.

Ela carrega uma entidade sem suas entidades relacionais e quando precisamos de entidades relacionais, o EF Core obtém dados relacionais com novas consultas do banco de dados. Usando esta estratégia, em vez de carregar todas as entidades desnecessárias de uma vez, ela carrega as entidades apenas quando precisamos delas. O carregamento lento pode reduzir o uso de memória e melhorar o desempenho do aplicativo.

Exemplo de habilitação do Lazy Loading:     
builder.Services.AddDbContext<MyContext>(options =>
                     options.UseLazyLoadingProxies());

Eager Loading:

 O Eager Loading é uma estratégia em que os dados relacionados são carregados antecipadamente em uma única consulta, evitando a necessidade de consultas adicionais quando os dados relacionados são acessados.

Todos os dados são carregados em uma consulta. Quando carregamos uma entidade, todas as entidades relacionais também são carregadas ao mesmo tempo. Se usarmos entidades relacionais com frequência, devemos preferir o carregamento antecipado.

Exemplo de Eager Loading usando Include:

  var blogs = context.Blogs
      .Include(blog => blog.Posts)
      .ToList();

Deve ser usado especialmente em cenários onde os dados são usados em loops. Essa estratégia pode ser boa para o desempenho. Se não precisamos de entidades relacionais, esta estratégia é ruim para o desempenho.

4- Consultas divididas (Split Queries)

Quando o EF Core trabalha com entidades relacionais, obtém dados adicionando consultas de junção em uma única consulta. Ele obtém todos os dados relacionais em uma consulta padrão e neste caso, pode ocorrer um grande conjunto de dados resultando em um produto cartesiano de tabelas relacionadas.

Por exemplo, se houver muitos relacionamentos entre duas tabelas e cada uma contiver centenas de registros, o conjunto de resultados resultante de uma única consulta combinada poderá ser bastante grande. Isso pode afetar negativamente o desempenho e causar recuperação desnecessária de dados.

O método AsSplitQuery() é usado para obter entidades relacionais com diferentes consultas. Desta forma, evitam-se problemas de desempenho causados por grandes conjuntos de dados e proporcionam-se consultas mais eficientes.

O recurso de "Split Queries" no Entity Framework Core permite que as consultas que retornam múltiplos conjuntos de resultados sejam divididas em consultas separadas. Essa funcionalidade foi introduzida para otimizar o desempenho e evitar a carga desnecessária de dados quando apenas uma parte do resultado é necessária.

Exemplo de Uso:

var blog = context.Blogs
    .Include(blog => blog.Posts.Take(5))
    .FirstOrDefault();

O EF Core dividirá essa consulta em duas partes: uma para carregar o blog e outra para carregar apenas os primeiros cinco posts.

O "Split Queries" é habilitado por padrão no EF Core. No entanto, você pode explicitamente habilitá-lo usando o método UseQuerySplitting().

builder.Services.AddDbContext<MyContext>(options =>
    options.UseSqlServer(connectionString)
           .UseQuerySplitting());

Evita a carga desnecessária de dados quando apenas parte do resultado é necessária, o que pode melhorar o desempenho e reduzir a sobrecarga de tráfego de rede.

Embora o recurso "Split Queries" seja útil em muitos cenários, é importante usá-lo com discernimento. Em algumas situações, pode ser mais eficiente carregar todos os dados em uma única consulta, especialmente se os conjuntos de dados forem pequenos ou se a maioria dos dados for necessária.

6- Utilize o AddDbContextPool

O objeto DbContext é um objeto leve. No entanto, a criação e o descarte da instância DbContext podem afetar negativamente o desempenho em alguns aplicativos.

Quando usamos o padrão de pool de objetos, o núcleo EF envia o DbContext para o pool. O EF Core mantém instâncias DbContext no pool na memória e se precisarmos de DbContext, podemos obter DbContext do pool.

O padrão de pool de objetos reduz o uso de memória. Com este método, um aumento notável no desempenho pode ser observado em projetos de alto tráfego.

AddDbContextPool é um método de extensão disponível no ASP.NET Core que permite adicionar um serviço de contexto do Entity Framework Core ao contêiner de injeção de dependência configurando-o para uso com um pool de instâncias.

builder.Services.AddDbContextPool<MyContext>(options =>
   options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

O pool de contextos ajuda a economizar recursos, pois reutiliza instâncias de contexto em vez de criar uma nova instância para cada solicitação. Isso é particularmente útil em cenários com grande concorrência.

Ao reutilizar instâncias de contexto, pode-se evitar o custo de criação e destruição de instâncias em cada solicitação, o que pode melhorar o desempenho, especialmente em ambientes com alto tráfego.

É possível configurar outras opções junto com AddDbContextPool da mesma forma que você faria com AddDbContext. Por exemplo, você pode configurar opções como o tempo limite do pool, a quantidade máxima de instâncias no pool, etc.

builder.Sservices.AddDbContextPool<MyContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
    options.PoolSize(50); // Define o tamanho máximo do pool
});

O contexto deve ser projetado de forma que seja seguro para uso com o pool. Evite armazenar estado de longa duração no contexto que possa causar problemas quando reutilizado.

Os serviços necessários no contexto, como serviços de escopo, devem ser resolvidos dentro do escopo para evitar problemas.

Se o lazy loading estiver habilitado, considere os impactos no desempenho e certifique-se de que está sendo usado com sabedoria, especialmente em cenários de pool de contexto.

O AddDbContextPool é particularmente útil em cenários de aplicativos web, onde há um grande número de solicitações concorrentes. No entanto, a eficácia do pool de contexto pode variar dependendo dos padrões de acesso ao banco de dados e da natureza do aplicativo.

E estamos conversados...

"Porque a minha mão fez todas estas coisas, e assim todas elas foram feitas, diz o Senhor; mas para esse olharei, para o pobre e abatido de espírito, e que treme da minha palavra."
Isaías 66:2

Referências:


José Carlos Macoratti