Back-End

14 jun, 2016

Entity Framework – Lazy Loading e o problema Select N+1

Publicidade

Neste artigo vou apresentar o problema conhecido como Select N+1, que afeta o desempenho da sua aplicação e que se manifesta quando se usa o Lazy Loading no Entity Framework.

Se você usa o Entity Framework em suas aplicações e têm percebido que elas estão muito lentas, você pode ser mais uma vítima do problema Select N+1, que é causado quando se usa o recurso do Lazy Loading, que vem habilitado por padrão, e que afeta de forma significativa aplicações que realizam consultas em entidades com associações um-para-muitos.

Se você ainda não conhece o Entity Framework muito bem, mas pretende usá-lo em suas aplicações, fique atento e acompanhe o artigo que pode livrá-lo de problemas futuros.

Nota: O problema Select N+1 não é um problema específico do Entity Framework; ele pode se manifestar em todos os frameworks ORM que usam o  Lazy Loading como padrão.

O que é Lazy Loading ou Lazy Load?

Lazy Load é o mecanismo utilizado pelos frameworks de persistência para carregar informações sobre demanda. Esse mecanismo torna as entidades mais leves, pois suas associações são carregadas apenas no momento em que o método que disponibiliza o dado associativo é chamado. Assim, quando objetos são retornados por uma consulta, os objetos relacionados não são carregados ao mesmo tempo. Na realidade, eles são carregados automaticamente quando a propriedade de navegação for acessada.

Assim, o mecanismo Lazy Load retarda a inicialização de um objeto até ao ponto que ele for realmente acessado pela primeira vez.

O objetivo do Lazy Load é fazer com que sua aplicação utilize menos memória e seja mais eficiente pela redução da quantidade de dados transferidos de/para o banco de dados.

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 quando estão envolvidas entidades com associações do tipo um-para-muitos.

A conclusão é que, infelizmente, esse recurso causa mais problemas do que benefícios – e, talvez, o pior problema causado seja o Select N+1.

Afinal o que é o problema conhecido como Select N +1?

Para ver como o problema se manifesta, vamos supor que temos em uma aplicação uma coleção de Filmes e que cada Filme tem uma coleção de atores, ou seja, temos duas entidades expressas no código abaixo:

Filmes.cs

public class Filme
    {
        public int FilmeId { get; set;  }
        public string Titulo { get; set;  }
        public ICollection<Ator> Atores { get; set;  }
    }

Ator.cs

public class Ator
    {
        public int AtorId { get; set; }
        public string Nome { get; set;  }
        public Filme Filme { get; set; }
    }

DBContexto.cs

public class DBContexto : DbContext
    {
        public DbSet<Filme> Filmes {get; set;}
        public DbSet<Ator> Atores { get; set; }
    }

Temos assim uma associação entre a entidade Filme e a entidade Ator do tipo um-para-muitos: um filme pode ter muitos atores.

Nota: O mapeamento será feito para as tabelas Filmes e Atores do banco de dados SQL Server.

Agora vamos supor que precisamos realizar uma consulta que retorne os filmes e, então, para cada filme precisamos retornar todos os atores.

Podemos realizar essa tarefa usando o código abaixo:

static void Main(string[] args)
   {
            using (var ctx = new DBContexto())
            {
                foreach (var filme in ctx.Filmes)
                {
                    foreach(var ator in ctx.Atores)
                    {
                        Console.WriteLine(" {0} : {1} ", filme.Titulo, ator.Nome);
                    }
                }
            }
 }

Aparentemente, não existe erro e o código vai compilar e vai funcionar normalmente, mas devido ao recurso Lazy Loading estar habilitado por padrão, o problema Select N+1 vai se manifestar.

Quando o código for executado, o Entity Framework irá gerar uma consulta SQL para retornar todos os filmes:

SELECT * FROM Filmes

e, a seguir, será gerada N consultas para obter cada ator do respectivo filme:

SELECT * FROM Atores WHERE FilmeId = 1
SELECT * FROM Atores WHERE FilmeId = 2
SELECT * FROM Atores WHERE FilmeId = 3
SELECT * FROM Atores WHERE FilmeId = 4
SELECT * FROM Atores WHERE FilmeId = 5
….

Ou seja, você terá um SELECT para Filmes e, então, N SELECTs adicionais para os atores.

Dessa forma, sua aplicação irá consultar o banco de dados N+1 vezes e isso vai degradar o desempenho da sua aplicação de forma drástica tanto quanto maior for o número de consultas. Esse é o famoso problema SELECT N +1 causado pelo Lazy Loading.

Imagine um cenário mais complexo com muitos relacionamentos e muitas consultas. É de arrepiar, não é mesmo?

Como evitar esse problema no Entity Framework?

Uma das formas de evitar esse problema no Entity Framework é usar o método Include e não usar o Lazy Loading. O método Include realiza o Eager Load na consulta, que é o mecanismo pelo qual uma associação, coleção ou atributo é carregado imediatamente quando o objeto principal é carregado.

Dessa forma, o código corrigido ficaria assim:

static void Main(string[] args)
   {
            using (var ctx = new DBContexto())
            {
                foreach (var filme in ctx.Filmes.Include("Atores"))
                {
                    foreach(var ator in ctx.Atores)
                    {
                        Console.WriteLine(" {0} : {1} ", filme.Titulo, ator.Nome);
                    }
                }
        }
 }

Este código agora usa o método Include para carregar todos os atores para cada filme e será gerado apenas uma consulta SQL contra o banco de dados.

Isso resolve o problema, mas você deve ficar atento ao usar o método Include, visto que ele gera consultas SQL fazendo um JOIN entre todas as tabelas que você deseja retornar – o que também pode afetar o desempenho.

Assim, antes de usar esse artifício, você tem que verificar quais dos dois caminhos afeta menos o desempenho da sua aplicação.

Você pode desabilitar o recurso Lazy Loading definindo a propriedade Configuration.LazyLoadingEnabled, assim como a false no construtor da sua classeDbContext.

No nosso exemplo podemos usar a instância do contexto para desabilitar o Lazy Loading: ctx.Configuration.LazyLoadingEnabled = false;

Para concluir, ficam aqui dois conselhos:

  1. Desabilite o Lazy Loading e verifique o desempenho usando Include (muito recomendado para aplicações web);
  2. Para retornar uma quantidade limitada de dados utilize consultas projeção;

Até o próximo artigo.