Entity Framework 6.x - Dicas para melhorar o desempenho - II
 Neste artigo vou apresentar 7 dicas para melhorar o desempenho do Entity Framework 6.x.

O Entity Framework 6.x é uma ferramenta ORM da Microsoft madura e testada pelo mercado que pode ser usada para aplicações que usam o .NET Framework.

Ela não é a bala de prata que vai resolver todos os seus problemas de acesso a banco de dados relacionais e seu uso deve ser considerado dependendo da sua expectativa e o cenário do seu ambiente de desenvolvimento.

Ela não é mais rápida que usar ADO .NET puro mas podemos melhorar o seu desempenho e para isso apresento aqui essas dicas.

1 - Filtre as entidades no banco de dados

Você já deve saber que podemos usar dois 'sabores' da LINQ para realizar consultas no Entity Framework:

//LINQ to Objects: Todos os clientes são retornados do banco de dados e filtrados na memória
var clientes = ctx.Clientes.ToList().Where(x => x.Nomes.Any());
//LINQ to Entities: os clientes são filtrados no banco de dados e e somente depois retornados para a memória
var clientes = ctx.Clientes.Where(x => x.Nomes.Any()).ToList();

As consultas feitas usando LINQ to Entities retornam coleções que são executadas imediatamente após você chamar um método ToList, ToArray ou ToDictionary. Após disso, o resultado é materializado e armazenado no espaço de memória do processo.

Como essas consultas são instâncias de IEnumerable<T>, elas podem ser manipuladas com operadores padrão LINQ to Objects sem que você perceba. Isso significa que usar LINQ to Entities faz com que a consulta seja executada no banco de dados enquanto que usar LINQ to Objects faz com que a consulta seja executada pelo processo atual, ou seja, na memória.

2 - Desabilite a monitoração do resultado das consultas de exibição de dados pelo contexto usando : AsNoTracking

O Entity Framework possui um cache de primeiro nível (ou local) onde todas as entidades conhecidas por um contexto EF, carregadas a partir de consultas ou explicitamente adicionadas, são armazenadas.

Isso acontece de forma que quando chegar a hora de salvar as alterações no banco de dados, o EF passa por esse cache e verifica quais entidades precisam ser salvas, ou seja, quais precisam ser inseridas, atualizadas ou excluídas.

O que acontece se você carregar um número de entidades apenas de consultas quando EF tem que salvar ?

Ele vai precisar passar por todos essas entidades, para verificar quais mudaram, e isso constitui em um aumento de memória.

Se você não precisa acompanhar as entidades que resultam de uma consulta, pois elas são apenas para exibição, você deve aplicar o método de extensão AsNoTracking.

//sem caching
var clientes = ctx.Clientes.AsNoTracking().ToList();
var clientesComPendencia = ctx.Clientes.Where(x => x.Pendencias.Any()).AsNoTracking().ToList();
var localClientes = ctx.Clientes.Local.Any(); 

 

Dessa forma a consulta será executa mais rápidamente pois o EF não tem que colocar cada resultado no cache.

Nota: Se você usar esse recurso em entidades que precisam se atualizadas (CRUD) você terá que anexá-las ao contexto e definir o seu estado atual antes de salvar as alterações.

3 - Desabilite a detecção automática de alterações nas entidades

Por padrão, o método DetectChanges é chamado automaticamente em várias ocasiões, como quando uma entidade é explicitamente adicionada ao contexto, quando uma consulta é executada, etc. Isso resulta na degradação do desempenho sempre que um grande número de entidades está sendo monitorado.

A solução alternativa é desativar o controle automático de alterações definindo a propriedade AutoDetectChangesEnabled com o valor adequado.

// desativando a monitoração automática de alterações
ctx.Configuration.AutoDetectChangesEnabled = false;

Lembre-se que sempre que SaveChanges for invocado, ele vai chamar DetectChanges e tudo vai funcionar corretamente.

Nota:  Esse recurso parece ter um impacto positivo somente que você usa Add para adicionar as entidades no contexto. Ele não faz diferença quando você usa AddRange.

4 - Utilize Lazy e Eager Loading de forma apropriada

O mecanismo Lazy Load retarda a inicialização de um objeto até ao ponto que ele for realmente acessado pela primeira vez.  Pela definição você pode achar que o Lazy Load é um bom recurso e o fato de ser habilitado por padrão parece que é um aval para sua utilização mas ele esconde um problema gravíssimo que se manifesta em consultas quanto estão envolvidas entidades com associações do tipo um-para-muitos.

O mais grave deles é conhecido como SELECT N+1 que se manifesta assim : você emite uma consulta base que retorna N elementos e, em seguida, serão emitdas outras N consultas, uma para cada referência/coleção que você deseja acessar.   Dessa forma quando você realmente precisar acessar uma propriedade de referência ou navegar através de todas as entidades filhas presentes em uma coleção, você deve carregá-las usando o Eager Loading com a entidade que as contém.

Para utilizar o Eager Loading você deve usar o método de extensão Include :

//eager load dos clientes para cada Recurso
var clientesRecursoInclude = ctx.Recursos.Include(x => x.Clientes).ToList();

//eager load do cliente para cada projeto
var clientesProjetosInclude = ctx.Projetos.Include("Cliente").ToList();

Dessa forma usando o Eager Loading você evita muitos problemas mas pode estar trazendo mais informações de que precisa.

Nota: Para saber mais sobre o problema SELECT N+1 veja o artigo : Entity Framework - O Lazy Loading e o problema Select +1 - Macoratti

5 - Utilize projeções sempre que possível

Não sei se você sabe mas normalmente as consultas LINQ e Entity SQL retornam entidades completas; Isto é, para cada entidade, eles trazem todas as suas propriedades mapeadas (exceto referências e coleções).

Às vezes não precisamos da entidade completa, mas apenas algumas partes dela, ou mesmo algo calculado de algumas partes da entidade. Para isso, usamos projeções.

As projeções nos permitem reduzir significativamente os dados retornados permitindo-nose escolher apenas aqueles que precisamos.

Aqui estão alguns exemplos em LINQ e Entity SQL.

//retorna os recursos e nome dos projetos somente com LINQ
var recursosProjetos = ctx.Projetos.SelectMany(x => x.ProjetoRecursos).Select(x => new { Recurso = x.Recurso.Nome, Projeto = x.Projeto.Nome }).ToList();
//retorna os nomes dos clientes e a quantidade de seus projetos com LINQ
var clientesQuantosProjetos = ctx.Clientes.Select(x => new { x.Nome, Count = x.Projetos.Count() }).ToList();
//retorna o nome do projeto e sua duração com ESQL
var projetoNomeDuracao = octx.CreateQuery<Object>("SELECT p.Nome,DIFFDAYS(p.Start, p.[End]) FROM Projetos AS p WHERE p.[End] IS NOT NULL").ToList();

As projeções usando LINQ dependem dos tipos anônimos, mas você também pode selecionar o resultado em uma classe .NET.

6 - Desabilite a validação após Salvar

As validações são acionadas para cada entidade quando o contexto está prestes a salvá-los, e se você tiver muitas entidades, isso pode levar algum tempo.

Quando tiver completa certeza de que as entidades que pretende armazenar são todas válidas, você pode desativar a sua validação, desabilitando a propriedade ValidateOnSaveEnabled.

//desabilita a validação automática após salvar
ctx.Configuration.ValidateOnSaveEnabled = false;

Alerta : Mas fique atento ! pois esta é uma configuração Global.

7 - Procure trabalhar com entidades desconectadas

Ao salvar uma entidade, se você precisar armazenar em uma propriedade uma referência de outra entidade para a qual você conhece a chave primária, ao invés de carregar a entidade com Entity Framework, basta atribuir a entidade a uma entidade vazia definindo apenas a propriedade identidade(Id) preenchida.

//salva um novo projeto referenciando um cliente existente
var novoProjeto = new Projeto { Nome = "Projeto Teste", Cliente = new Cliente { ClienteId = 1 } /* ao invés de ctx.Clientes.Find(1)*/ };
ctx.Projetos.Add(novoProjeto);
ctx.SaveChanges();

Esse código vai funcionar pois o Entity Framework precisa somente da definição da chave estrangeira.

Da mesma forma, realizar uma exclusão você não precisa carregar a entidade previamente. Informe sua chave primária para fazer isso:

//deleta um Cliente pelo id
//ctx.Clientes.Remove(ctx.Clientes.Find(1));
ctx.Entry(novo Cliente { ClienteId = 1 }).State = EntityState.Deleted;
ctx.SaveChanges();

O objetivo dessas dicas é melhorar o desempenho do Entity Framework. Faça uma análise do seu projeto e verifique se alguma delas pode ser aplicada.

Após aplicar sempre teste o seu código, OK.

Porque todos quantos fostes batizados em Cristo já vos revestistes de Cristo.
Nisto não há judeu nem grego; não há servo nem livre; não há macho nem fêmea; porque todos vós sois um em Cristo Jesus.
Gálatas 3:27-28

Referências:


José Carlos Macoratti