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

  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 e pretende usá-lo em suas aplicações fique atento e acompanhe o artigo que pode livrá-lo 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, ao invés, 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 quanto estão envolvidas entidades com associações do tipo um-para-muitos.

A conclusão é que infelizmente esse recurso causa mais problemas do que os benefícios a que se propõe, e talvez o pior problema causado é conhecido como 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:

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

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

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

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.

Eager Load é 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 resolver 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 e isso 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 como igual a false no construtor da sua classe DbContext.

No nosso exemplo podemos usar a instância do contexto para desabilitar o Lazy Loading:

        ctx.Configuration.LazyLoadingEnabled = false;

Para concluir fica 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;

Ate o próximo artigo.

E ele lhes disse: Na verdade vos digo que ninguém há, que tenha deixado casa, ou pais, ou irmãos, ou mulher, ou filhos, pelo reino de Deus,
Que não haja de receber muito mais neste mundo, e na idade vindoura a vida eterna.
Lucas 18:29,30

Veja os Destaques e novidades do SUPER DVD Visual Basic (sempre atualizado) : clique e confira !

Quer migrar para o VB .NET ?

Quer aprender C# ??

Quer aprender os conceitos da Programação Orientada a objetos ?

Quer aprender o gerar relatórios com o ReportViewer no VS 2013 ?

Quer aprender a criar aplicações Web Dinâmicas usando a ASP .NET MVC 5 ?

  Gostou ?   Compartilhe no Facebook   Compartilhe no Twitter

Referências:


José Carlos Macoratti