Entity Framework – Carregando dados relacionados

Existem três abordagens diferentes que podemos utilizar para realizar o load de dados relacionados entre entidades. Essas três abordagens atingem os mesmos resultados, mas existem diferenças entre elas. Tais diferenças fazem com que cada abordagem seja melhor empregada em uma situação diferente das demais.

Não acredito que escolher uma das três abordagens e usá-la em todos os casos seja vantajoso. Acredito que o ideal é conhecer o funcionamento de cada uma das técnicas e reconhecer quando utilizar cada uma delas.

clip_image002

Veremos a seguir as características e o uso de cada uma destas abordagens.

Eager Loading

Eager Loading carrega os dados relacionados a uma entidade por meio da parametrização dos campos que devem ser carregados. Essa parametrização ocorre no momento da query e influência no modo como o Entity Framework irá compor sua consulta SQL. O uso de Eager Loading acarreta na composição de uma consulta com JOIN para outras tabelas, não apenas selecionando os registros da tabela foco da consulta, mas também nas tabelas relacionadas.

A indicação de quais campos devem ser carregados pela consulta ocorre durante a escrita da query, por meio do método “Include”. Tomemos como exemplo a query abaixo:

Estado result = null;

using (var contexto = new Contexto())
{
    var query = from e in contexto.Estados
                                    .Include(e => e.Cidades)
                                    .Include(e => e.Pais)
                where e.Nome == "São Paulo"
                select e;

    result = query.Single();
}

Nesta query estamos realizando uma consulta sobre a entidade “Estados” e indicando que queremos carregar os dados do “País” e das “Cidades” relacionadas a cada um destes “Estados”. Desta maneira, o Entity Framework irá compor uma consulta que tenha o relacionamento entre as tabelas de “Estados”, “Países” e “Cidades”.

A vantagem deste tipo de consulta é que uma única query é enviada ao banco de dados, compondo todas as instâncias de “Estado” com suas respectivas “Cidades” e seu “País”. Em compensação, deve-se saber que mais e mais dados agregados acarretam em uma query mais complexa, que de certa forma é maior e mais lenta de ser executada.

Lazy Loading

Lazy Loading é o modelo mais transparente, pois o Entity Framework fica responsável por fazer o load automático dos dados relacionados à entidade quando tentamos acessá-los, sem exigir que seja escrita nenhuma linha de código em nossas queries. Com o Lazy Loading, o EF executa queries automaticamente sempre que uma propriedade que contém dados relacionados for acessada, passando a impressão de que a propriedade sempre está carregada.

O EF tem a capacidade de criar proxies dinâmicos que herdam das entidades e sobrescrevem as propriedades que possuem dados relacionados com outra entidade (por isso o motivo das propriedades serem virtuais), adicionando uma lógica de consulta dos dados relacionados.

Para a implementação do Lazy Loading existem duas exigências básicas:

1 – Nossa entidade deve ser uma classe POCO publica (public) e que não seja sealed;

2 – A propriedade de navegação deve ser marcada como virtual.

Na nossa estrutura de classes precisamos apenas modificar as propriedades que possuem dados relacionados, tornando-as virtuais, como no exemplo abaixo:

        public virtual IList<Cidade> Cidades { get; set; }

        public virtual Pais Pais { get; set; }

A query utilizando os recursos de Lazy Loading não exige o uso de nenhum recurso adicional, apenas o ajuste das propriedades de navegação, por esse motivo o Lazy Loading é tão transparente. Abaixo a query que faz uso dos recursos de Lazy Loading.

using (var context = new Contexto())
{
    var query = from e in context.Estados
                where e.Nome == "São Paulo"
                select e;
                
    result = query.Single();
}

Em contraponto, o uso inadequado de Lazy Loading é bastante perigoso, pois pode acarretar em um número excessivo de consultas enviadas ao banco de dados, pois não temos controle da execução das queries que serão executadas pelas classes proxies. Assim, o número de idas e vindas ao banco de dados é muito maior.

Supondo que uma consulta de Paises retorne 10 países, cada vez que acessarmos a propriedade “Estados” uma nova consulta será enviada ao banco de dados. Desta forma, se acessarmos a propriedade “Estados” destes 10 países, serão enviadas 11 consultas para o banco de dados (uma consulta para retornar os dez países e mais dez consultas para retornar os estados de cada país).

Muitas vezes, neste cenário, é muito mais vantajoso carregar os dados a partir de uma única query que faça JOIN com outras tabelas e retorne toda a massa de dados que precisamos.

Outra dica é: se preferir desabilitar o uso do Lazy Loading, existe uma propriedade que desativa o uso de Lazy Loading. O exemplo abaixo demonstra seu uso.

    using (var context = new Contexto())
    {
        context.Configuration.LazyLoadingEnabled = false;

        var query = from e in context.Estados
                    where e.Nome == "São Paulo"
                    select e;
                
        result = query.Single();
    }

Explicit Loading

Assim como o Lazy Loading, o Explicit Loading carrega os dados relacionados separadamente, após os principais dados terem sido carregados. Mas, de forma contrária ao Lazy Loading, este carregamento dos dados relacionados não ocorre de maneira automática e não exige o uso de propriedades virtuais.

A principal vantagem desta abordagem em relação ao Lazy Loading é que sabemos exatamente a quantidade de consultas e o momento no qual as consultas são enviadas para o banco de dados.

Abaixo um exemplo de query utilizando Explicit Loading:

Estado result = null;

using (var contexto = new Contexto())
{
    var query = from e in contexto.Estados
                where e.Nome == "São Paulo"
                select e;
                                
    result = query.Single();

    contexto.Entry(result).Collection(e => e.Cidades)
                            .Load();
    contexto.Entry(result).Reference(e => e.Pais)
                            .Load();
}

Vale a pena notar que a indicação de quais dados devem ser carregados parte do uso do método “DbContext.Entry” (melhor detalhado neste link: https://ferhenriquef.com/2012/10/15/dbentityentry-capturando-os-dados-originais-de-uma-entidade/). Notamos também que propriedades que são coleções fazem uso do método “Collection”, enquanto que as propriedades de navegação fazer uso do método “Reference”.

Conclusão

Podemos notar que estas três abordagens possuem usos e implicações distintas. Podemos ver que vale a pena avaliar o cenário de nossas consultas e utilizar uma abordagem de load dos dados relacionados mais apropriada para caso. Eager Loading retornará todos os dados relacionados as entidades juntos em uma única query, isso significa que: enquanto uma única conexão é feita com o banco de dados, uma grande quantidade de dados é retornada, por conta da necessidade de adicionar JOINs com outras tabelas. Em contrapartida, Explicit e Lazy Loading permitem que os dados relacionados sejam carregados somente quando necessários. Entretando, essas abordagens exigem que cada consulta por dados relacionados faça uma nova conexão com o banco de dados, além da execução de uma nova query.

Obs.: após as referências do post será postado o código utilizado para construção dos cenários de teste.

Por

MSc. Fernando Henrique Inocêncio Borba Ferreira

Microsoft Most Valuable Professional – Visual C#

Referências:

“DbContext – Programming Entity Framework” – J. Lerman & R. Miller.

Loading Related Entities – http://msdn.microsoft.com/en-us/data/jj574232.aspx

Demystifying Entity Framework Strategies: Loading Related Data – http://msdn.microsoft.com/en-us/magazine/hh205756.aspx

Loading Related Objects – http://msdn.microsoft.com/en-us/library/vstudio/bb896272(v=vs.100).aspx



Anexo – Código utilizado para construção dos cenários de teste

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace EFConsoleApp
{
    public class Pais
    {
        public int Id { get; set; }

        public string Nome { get; set; }
    }

    public class Estado
    {
        public int Id { get; set; }

        public string Nome { get; set; }
        
        // To execute searchs with Lazy Loading you need to change
        // the property struct to use VIRTUAL feature...
        public /*virtual*/ IList<Cidade> Cidades { get; set; }

        public /*virtual*/ Pais Pais { get; set; }
    }

    public class Cidade
    {
        public int Id { get; set; }

        public string Nome { get; set; }
    }

    public class Contexto : DbContext
    {
        public DbSet<Pais> Paises { get; set; }

        public DbSet<Estado> Estados { get; set; }

        public DbSet<Cidade> Cidades { get; set; }

        public Contexto()
        {
            Database.SetInitializer<Contexto>(new DropCreateDatabaseIfModelChanges<Contexto>());
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Pais>().ToTable("Pais");
            modelBuilder.Entity<Estado>().ToTable("Estado");
            modelBuilder.Entity<Cidade>().ToTable("Cidade");

            base.OnModelCreating(modelBuilder);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Verify is SeedData routine needs to run...
            if (NeedToSeedData())
                SeedData(); // Create default values in database

            // Execute search using Lazy Loading
            SelectDataUsingLazyLoading();

            // Execute search using Eager Loading
            SelectDataUsingEagerLoading();

            // Execute search using Explicit Loading
            SelectDataUsingExplicitLoading();
        }

        /// <summary>
        /// Execute search using Lazy Loading
        /// </summary>
        static void SelectDataUsingLazyLoading()
        {
            // IMPORTANT!!!!
            // To execute this sample correctly you need to change the "Cidades" and "Pais" properties
            // to user the VIRTUAL characteristic...

            Estado result = null;

            using (var context = new Contexto())
            {
                var query = from e in context.Estados
                            where e.Nome == "São Paulo"
                            select e;
                
                result = query.Single();
            }

            if (result.Cidades != null || result.Pais != null)
                Console.WriteLine("Lazy loading works fine!!!! It loads my 'Cidades' and 'Pais' data!!! ");
            else
            {
                Console.WriteLine("Lazy loading didn't work fine!!!! It didn't load 'Cidades' and 'Pais' data!!! ");
                Console.WriteLine("Verify VIRTUAL properties!!! ");
            }
        }

        /// <summary>
        /// Execute search using Eager Loading
        /// </summary>
        static void SelectDataUsingEagerLoading()
        {
            Estado result = null;

            using (var contexto = new Contexto())
            {
                var query = from e in contexto.Estados
                                                .Include(e => e.Cidades)
                                                .Include(e => e.Pais)
                            where e.Nome == "São Paulo"
                            select e;

                result = query.Single();
            }

            if (result.Cidades != null || result.Pais != null)
                Console.WriteLine("Eager loading works fine!!!! It loads my 'Cidades' and 'Pais' data!!! ");
            else
                Console.WriteLine("Eager loading didn't work fine!!!! It didn't load 'Cidades' and 'Pais' data!!! ");
        }

        /// <summary>
        /// Execute search using Explicit Loading
        /// </summary>
        static void SelectDataUsingExplicitLoading()
        {
            Estado result = null;

            using (var contexto = new Contexto())
            {
                var query = from e in contexto.Estados
                            where e.Nome == "São Paulo"
                            select e;
                                
                result = query.Single();

                contexto.Entry(result).Collection(e => e.Cidades)
                                        .Load();
                contexto.Entry(result).Reference(e => e.Pais)
                                        .Load();
            }

            if (result.Cidades != null || result.Pais != null)
                Console.WriteLine("Explicit loading works fine!!!! It loads my 'Cidades' and 'Pais' data!!! ");
            else
                Console.WriteLine("Explicit loading didn't work fine!!!! It didn't load 'Cidades' and 'Pais' data!!! ");
        }

        /// <summary>
        /// Verify if the 'SeedData' needs to run.
        /// </summary>
        /// <returns></returns>
        static bool NeedToSeedData()
        {
            bool returnValue = true;

            using (var contexto = new Contexto())
            {
                var qtdeEst = (from e in contexto.Estados
                               select e).Count();

                if (qtdeEst > 0)
                    returnValue = false;
            }

            return returnValue;
        }

        /// <summary>
        /// Create initial data to execute diferrent loading approachs.
        /// </summary>
        static void SeedData()
        {
            #region [ Brasil ]

            Pais brasil = new Pais();
            brasil.Nome = "República Federativa do Brasil";

            #endregion

            #region [ Rio de Janeiro ]

            Estado rioJaneiro = new Estado();
            rioJaneiro.Nome = "Rio de Janeiro";
            rioJaneiro.Pais = brasil;
            rioJaneiro.Cidades = new List<Cidade>();
            rioJaneiro.Cidades.Add(new Cidade() { Nome = "Rio de Janeiro" });
            rioJaneiro.Cidades.Add(new Cidade() { Nome = "Agulhas negras" });
            rioJaneiro.Cidades.Add(new Cidade() { Nome = "Parati" });
            
            #endregion

            #region [ São Paulo ]

            Estado saoPaulo = new Estado();
            saoPaulo.Nome = "São Paulo";
            saoPaulo.Pais = brasil;
            saoPaulo.Cidades = new List<Cidade>();
            saoPaulo.Cidades.Add(new Cidade() { Nome = "São Paulo" });
            saoPaulo.Cidades.Add(new Cidade() { Nome = "Ribeirão Preto" });
            saoPaulo.Cidades.Add(new Cidade() { Nome = "Campinas" });

            #endregion

            using (var contexto = new Contexto())
            {
                contexto.Estados.Add(rioJaneiro);
                contexto.Estados.Add(saoPaulo);

                contexto.SaveChanges();
            }
        }
    }
}
Publicidade

23 comentários sobre “Entity Framework – Carregando dados relacionados

  1. Muito bom este post, parabéns. Tenho uma dúvida que pode não ter relação direta com este post, mas se puder me ajudar… vai lá… Em pesquisas sobre o desempenho do EF vi algumas pessoas com problemas quando seus contextos não são pequenos, estive a procura de alguma documentação do EF que recomende que o contexto não seja grande, ou um limite para o contexto, até vi algumas publicações da Julia Lerman sobre a utilização do EF com DDD, para diminuir os contextos, mas em nenhum momento vi isto como um recomendação técnica, indicando a recomendação de entidades por contexto, estou trabalhando em um projeto que a principio já esta com 2000 entidades, podem haver pelo menos 200 entidades por módulo, seria recomendável diminuir ainda mais este número de entidades por contexto, há alguma recomendação sobre este assunto?

    • Olá Ricardo,
      Obrigado por seus comentários.

      Sim, realmente existe a recomendação de não utilizar contextos muitos grandes. Eu não trabalho com contextos com mais de 50 entidades, pois sua inicialização se torna muito lenta. Eu acredito já ter lido um parágrafo (sim, um único parágrafo) citando essa questão do tamanho dos contextos, mas não me lembro onde foi. Vou pesquisar nos meus livros e te retorno.
      Pode ser? 🙂

      []s!

      • Claro… muito obrigado pela resposta… já me ajudou muito. Será que o problema é só na hora da inicialização? pois desta forma iria avaliando se a demora do mesmo esta aceitável, sendo que esta inicialização acontece uma única vez…

      • Oi Ricardo,
        Existem outros problemas adicionais tb. Problemas que estão relacionado com a leitura do schema mapeado pelo contexto, pois um contexto com mais entidades tem mais entidades para avaliar durante a construção das queries. Outra questão questão é que um contexto com muitas entidades armazena mais dados em cache, o que é ruim e consome mais memória.

        Existem bastante coisas relacionadas.

        Obrigado por postar.

        []s!

  2. Olá Fernando, ótimo post.
    Se possível, me tire uma dúvida.
    Existe alguma forma de relacionarmos no EF tabelas que não estão relacionadas no banco?
    Abs.

    • O EF framework não “mapeia” chaves estrangeiras fisicamente, existe apenas as propriedades de navegação (relacionamentos) de forma lógica. Para navegar as propriedades complexas de uma entidade, não é necessária a existência das chaves estrangeiras no banco de dados. Mas por questões de performance, é altamente recomendável. Estou certo Fernando ?

      • opa! Sim, isso mesmo. O EF faz o mapeamento lógico dos relacionamento, não o físico. Então vc pode “enganar” o mapeamento afirmando que existe um relacionamento físico entre duas tabelas que na verdade não existe.

  3. Belo artigo…uma dúvida se puderem ajudar….
    Para o cenário, onde eu tenha tabela A se relacionando com B, onde B é um tipo de A. B se relaciona com C onde é MpM e C se relaciona com D e F com relacionamento FK. Como fazer um insert único para A B e C. Será que ficou claro ?
    abç

  4. Em questão de performance, em um sistema de médio porte, com mais ou menos 50 tabelas, é recomendável usar o code first ou database first, verificando vi que da para controlar a versão do banco de dados no code first que vai para o cliente através do entity migrations,

    Qual sistema recomendaria?

    • Olá Ricardo,
      Atualmente, o code first e o database first utilizam o mesmo core. Desta forma, eles possuem rendimento semelhante.
      O indicado é utilizar a versão 5 do Entity Framework com a última versão do .Net Framework, no caso a 4.5. Essa combinação é 67% mais rápida do que a combinação entre o Entity Framework 4 e o .Net Framework 4.

      Obrigado por postar.
      []s!

      • Obrigado. Tirou uma Grande dúvida.

        Mais voçê recomendaria o code first no meu caso.

        Mesmo com stored procedures sendo executadas no banco.

  5. Olá Ricardo,
    Sim, eu sempre recomendo o Code First, pois prefiro as vantagens que ele agrega para o projeto, mesmo com stored procedures.

    Obrigado por postar.
    []s!

  6. Olá! Só passei pra dizer que o artigo é ótimo e acabou de me ajudar a resolver um problemão aqui com um projeto! Obrigado! 😀

  7. Eu acredito que só faltou você informar quando vale a pena utilizar cada uma dessas opções… os possíveis critérios para se decidir isso.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.