Entity Framework Core - Otimizando Consultas


Hoje vamos lembrar de alguns recursos do EF Core que podem ajudar a melhorar o desempenho das consultas.

Atualmente (junho/2020) a versão estável do EF Core é a versão 3.1.4, mas já temos as versões preview do EF core 5.0.0 conforme você pode conferir aqui. (Na verdade o EF Core 5.0 ja foi lançado quando este artigo foi publicado)

Para poder mostrar algumas dicas de desempenho eu vou criar um projeto Console (.NET Core) no no VS 2019 Community usando o EF Core. Para isso as etapas a serem cumpridas serão as seguintes:

Ao final termos uma aplicação onde podemos mostrar as dicas do EF Core.

1- A primeira dica refere-se à otimização das consultas que não envolvem a atualização de dados usando AsNoTracking

O EF Core atua de duas maneira para ler dados do banco de dados :

  1. Usando a consulta LINQ normal;
  2. Usando uma consulta LINQ que contém o método AsNoTracking;

Abaixo temos um exemplo onde estamos retornando todos os produtos:

           using (var _context = new AppDbContext())
           {
             
  var produtos = _context.Products.ToList();

                foreach (var p in produtos)
                {
                    Console.WriteLine($"{p.ProductId} \t {p.ProductName} \t\t {p.UnitPrice}");
                }
            }

Ambos os tipos de consultas retornam classes (as classes entity) com links para outras classes entity, conhecidas como propriedades de navegação que são carregadas ao mesmo tempo.

Na consulta normal o EF Core também faz uma cópia dos dados lidos dentro do contexto da aplicação para gerenciar as entidades (Change Tracker) e assim permitir que elas sejam atualizadas no banco de dados e isso tem um impacto no desempenho.

Podemos otimizar a consulta e desativar o Change Tracker usando o método AsNoTracking():

           using (var _context = new AppDbContext())
           {
             
  var produtos = _context.Products.AsNoTracking().ToList();

                foreach (var p in produtos)
                {
                    Console.WriteLine($"{p.ProductId} \t {p.ProductName} \t\t {p.UnitPrice}");
                }
            }

Com isso o desempenho da consulta será melhor, mas isso só vale para consultas somente leitura.

2- A segunda dica refere-se a carga de entidades relacionadas

Aqui temos duas opções:

a- Eager Loading usando o método Include()

            using (var _context = new AppDbContext())
            {
                var customer = _context.Customers
                                       .Include(c => c.Orders)
                                       .SingleOrDefault(c => c.CustomerId == "ANTON");
                Console.WriteLine($"{customer.CustomerId} {customer.ContactName}");

                foreach(var order in customer.Orders)
                {
                    Console.WriteLine($"{order.OrderId} {order.OrderDate}");
                }
            }
            Console.ReadLine();

O Eager Loading ou carregamento antecipado é o processo pelo qual uma consulta para um tipo de entidade também carrega entidades relacionadas como parte da consulta.

O carregamento ágil é obtido pelo uso do método Include. Isso significa que a solicitação de dados relacionados será retornada junto com os resultados da mesma consulta do banco de dados. Se este for o cenário ideal para o seu caso considere usar o eager loading.

No entanto existem ocasiões que é mais vantajoso aguardar e carregar todos dados até que eles sejam realmente necessários. Neste caso temos a segunda abordagem:

b- Usando Lazy Loading

            using (var _context = new AppDbContext())
            {
                var customer = _context.Customers
                                       .SingleOrDefault(c => c.CustomerId == "ANTON");
                Console.WriteLine($"{customer.CustomerId} {customer.ContactName}");

                foreach(var order in customer.Orders)
                {
                    Console.WriteLine($"{order.OrderId} {order.OrderDate}");
                }
            }
            Console.ReadLine();

O Lazy Loading ou 'Carregamento preguiçoso' é um recurso onde as entidades relacionadas no modelo de entidades são carregadas nas consultas sob demanda, ou seja, somente quando forem realmente necessárias e isso ocorre quando usamos um First, Find, Single, Find, ToList, ToArray ou quando iteramos usando o foreach ou for/next.

A maneira mais simples para usar o Lazy Loading é :

  1. Instalar o pacote Microsoft.EntityFrameworkCore.Proxies ;
  2. Habilitá-lo com uma chamada para UseLazyLoadingProxies;

Para a nossa aplicação console isso é feito assim:

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseLazyLoadingProxies()
                .UseSqlServer(@"Data Source=Macoratti;Initial Catalog=Northwind;Integrated Security=True");
        }

Após isso o EF Core, vai habilitar o lazy loading para qualquer propriedade de navegação que pode ser sobrescrita, ou seja, a propriedade de navegação deve ser virtual, e estar em uma classe que pode ser herdada.

            using (var _context = new AppDbContext())
            {
                var customer = _context.Customers
                                       .SingleOrDefault(c => c.CustomerId == "ANTON");
                Console.WriteLine($"{customer.CustomerId} {customer.ContactName}");

                foreach(var order in customer.Orders)
                {
                    Console.WriteLine($"{order.OrderId} {order.OrderDate}");
                }
            }
            Console.ReadLine();

Agora não precisamos mais usar o include para carregar os dados relacionados que serão carregados à medida que forem necessários.

Aqui caberá a você decidir qual delas obtêm um melhor desempenho para o seu cenário.

3- A terceira dica refere-se a utilização de suas próprias consultas SQL

Aqui temos duas opções :

  1. Usar fromSqlInterpolated  que usa strings interpoladas e previne o ataque de injeção SQL:
            using (var _context = new AppDbContext())
            {
                var preco = 39.50m;

                var produtos = 
                  _context.Products
                  .FromSqlInterpolated($"SELECT * FROM dbo.Products Where UnitPrice > {preco}")
                  .ToList();
                foreach (var p in produtos)
                {
                    Console.WriteLine($"{p.ProductId} {p.ProductName} {p.UnitPrice}" );
                }
            }

2- Usar o método de extensão FromSqlRaw para iniciar uma consulta LINQ com base em uma consulta SQL bruta.

Nota: FromSqlRaw só pode ser usado em raízes de consulta, diretamente no DbSet<T>.

            using (var _context = new AppDbContext())
            {
                var preco = 39.50m;

                var produtos = 
                  _context.Products
                  .FromSqlRaw("SELECT * FROM dbo.Products where UnitPrice > {0}", preco)
                  .ToList();
                foreach (var p in produtos)
                {
                    Console.WriteLine($"{p.ProductId} {p.ProductName} {p.UnitPrice}" );
                }
            }

Usando o método FromSqlRaw também podemos executar procedimentos armazenados.

Lembrando que as consultas que usam os métodos FromSqlRaw ou FromSqlInterpolated seguem exatamente as mesmas regras de controle de alterações que qualquer outra consulta LINQ no EF Core.

Existem algumas limitações a serem observadas ao usar esses métodos com consultas SQL:

Assim você pode definir sua própria consulta otimizada e usá-la para melhorar o desempenho.

E estamos conversados...

Pegue o projeto aqui: ConsoleEFCore1.zip (sem as referências)

(Porque a vida foi manifestada, e nós a vimos, e testificamos dela, e vos anunciamos a vida eterna, que estava com o Pai, e nos foi manifestada);
1 João 1:2

Referências:


José Carlos Macoratti