Entity Framework e lazy loading
Autor: Luciano Moreira | Publicado: 2016-07-28T10:34:00-03:00

A cada dia que passa fica mais comum encontramos soluções com camadas de mapeamento objeto-relacional (ORM), simplificando o acesso a dados para os desenvolvedores.
Ressalto que não sou contra o Entity Framework, hibernate ou outros frameworks de mapeamento objeto relacional, só quero que as pessoas que o utilizam tomem cuidados básicos no acesso a dados, pois não existe mágica…


Banco de dados

Utilize o script abaixo para criar um banco de dados de exemplo com três tabelas, Pessoa, Endereço e Telefone, inserindo&nbsp;também&nbsp;alguns registros.
Ok, eu sei que você modelaria diferente, pois uma pessoa pode ter mais de um endereço ou telefone, mas para este exemplo a regra de negócio da Lutilândia não permite.

CREATE DATABASE EF
GO
USE EF
GO
IF (OBJECT_ID('dbo.Pessoa') IS NOT NULL)
 DROP TABLE dbo.Pessoa
go
IF (OBJECT_ID('dbo.Endereco') IS NOT NULL)
 DROP TABLE dbo.Endereco
GO
IF (OBJECT_ID('dbo.Telefone') IS NOT NULL)
 DROP TABLE dbo.Telefone
go

CREATE TABLE dbo.Endereco
(ID INT IDENTITY NOT NULL PRIMARY KEY,
&nbsp;Descricao VARCHAR(500) NOT NULL)
GO

CREATE TABLE dbo.Telefone
(ID INT IDENTITY NOT NULL PRIMARY KEY,
&nbsp;Descricao VARCHAR(500) NOT NULL)
GO

CREATE TABLE dbo.Pessoa
(Codigo INT IDENTITY NOT NULL PRIMARY KEY,
&nbsp;Nome VARCHAR(100) NOT NULL,
&nbsp;IdEndereco INT NULL,
&nbsp;IdTelefone INT NULL
&nbsp;)
GO

ALTER TABLE dbo.Pessoa
ADD CONSTRAINT fk_Pessoa_Endereco_ID&nbsp;
FOREIGN KEY (IdEndereco)
REFERENCES dbo.Endereco (ID)
GO

ALTER TABLE dbo.Pessoa
ADD CONSTRAINT fk_Pessoa_Telefone_ID&nbsp;
FOREIGN KEY (IdTelefone)
REFERENCES dbo.Telefone (ID)
GO

INSERT INTO dbo.Endereco (Descricao)
SELECT TOP 100000 O.description
FROM sys.dm_xe_map_values AS M
CROSS JOIN sys.dm_xe_objects AS O
GO

INSERT INTO dbo.Telefone (Descricao)
SELECT TOP 100000 O.description
FROM sys.dm_xe_map_values AS M
CROSS JOIN sys.dm_xe_objects AS O
GO

INSERT INTO dbo.Pessoa (Nome)
SELECT TOP 1000000 M.name
FROM sys.dm_xe_map_values AS M
CROSS JOIN sys.dm_xe_objects AS O
GO

UPDATE dbo.Pessoa
SET IdEndereco = (Codigo % 100000) + 1
 , IdTelefone = (Codigo % 100000) + 1
GO


Lazy loading
Utilizando o Entity Framework 6.0, o comportamento padrão do EF é que o contexto trabalhe com lazy loading. O que isso significa?

Para quem está seguindo o script, crie um projeto C# Console no Visual Studio e…

&nbsp; &nbsp; 1. Adicione um ADO.NET Entity Data Model (chame do que quiser)
&nbsp; &nbsp; 2. Escolha EF Designer from database
&nbsp; &nbsp; 3. Aponte para seu banco de dados EF (pode manter o nome EFEntities)
&nbsp; &nbsp; 4. Selecione as tabelas Pessoa, Endereco e Telefone
&nbsp; &nbsp; 5. Clique em Finish
 
Depois da geração do modelo você vai ver algo similar a imagem abaixo:





Edite seu método Main e coloque o código abaixo. O objetivo do código é buscar quase 10000 pessoas e depois fazer algo simples, acessando o endereço e telefone do indivíduo (note que não me interessa gerar saída alguma, só o acesso ao banco de dados).

String s = "";
DateTime dt = DateTime.Now;

EFEntities contexto = new EFEntities();
var r = contexto.Pessoas.Where(x =&gt; x.Codigo &lt; 10000);

foreach (var p in r.ToList())
{
&nbsp; &nbsp; s = "Nome: " + p.Nome + " - Endereço: " + p.Endereco.Descricao + " - Telefone: " + p.Telefone.Descricao;
}
Console.WriteLine(DateTime.Now.Subtract(dt).Seconds.ToString());

A execução dessa aplicação na minha máquina levou algo em torno de 12 segundos, nada demais para o desenvolvedor que está executando este código, então poucos fazem uma análise mais detalhada, como faremos a seguir.

Utilizando o profiler para monitorar os eventos SQL:BatchCompleted e RPC:Completed, toda vez que o Entity Framework acessa a coluna Descrição da entidade Endereco ou Telefone, uma chamada é feita para o SQL Server (note o @EntityKeyValue1 variando), o que neste exemplo gerou 39998 entradas no profiler.

Essas chamadas são feitas porque o EF vai "preguiçosamente" (lazy) pedindo para o SQL Server os elementos de outras entidades, varrendo o resultado retornado após o acesso ao elemento Pessoa (primeira consulta exibida na imagem abaixo).





Eager loading
Caso você queira que o Entity Framework não trabalhe com esse comportamento de lazy loading, você pode alterar no seu código a utilização do contexto, adicionando o método "Include", que por debaixo dos planos já busca todos os elementos.

var r = contexto.Pessoas.Include("Endereco").Include("Telefone").Where(x =&gt; x.Codigo &lt; 10000);

Analisando a execução deste novo código, temos APENAS DUAS linhas na saída do profiler, onde notamos que o EF enviou para o SQL Server uma única consulta fazendo o join entre as três tabelas, já retornando o endereço e telefone. Dessa forma evitamos N round-trips entre o cliente e o servidor, fazendo com que em minha máquina o tempo total de execução ficasse em torno de 3 segundos.





Desempenho da aplicação

A diferença entre 3 e 12 segundos é onde mora o perigo... Talvez testando a solução em sua máquina, os 9 segundos não sejam significativos para os desenvolvedores, e um build sem a análise de desempenho ou do número de chamadas acabe chegando em produção.

Se a solução é para controlar o nome dos envolvidos no próximo bolão do campeonato lá na sua empresa, tudo tranquilo, porém caso a solução tenha milhares de requisições pesquisando por pessoas específicas, o número de chamadas ao banco de dados será muito grande. E dependendo da forma como a aplicação está gerenciando e utilizando os seus recursos (ou consumindo os registros), ela mesmo pode ser tornar o gargalo, que não respondendo adequadamente vai deixar o SQL Server cheio de requisições com espera do tipo ASYNC_NETWORK_IO (e NÃO É PROBLEMA DE REDE, ok?!!).

É meio óbvio que para escrever este artigo eu já encontrei diversos problemas de desempenho em aplicações por conta do Lazy Loading. O que em um primeiro momento sempre foi apontado como problema no banco de dados, conseguimos rastrear até apontar o comportamento indevido da aplicação. Então peço para que façam uma análise mais criteriosa de como o seu framework de mapeamento OR acessa o banco de dados, sempre entendendo o perfil das requisições e como pode utilizar da melhor forma o framework adotado.

Então estou falando para nunca usar lazy loading ou até configurar o contexto (this.Configuration.LazyLoadingEnabled = false) para impedir que um desenvolvedor desavisado faça uso do lazy load?
Claro que não, quero que você entenda que o maior custo de IO e CPU visto em uma única consulta com eager loading pode ser muito inferior quando comparado ao tempo total de execução do round-trip de milhares de chamadas para o SQL Server.

No fim tudo depende, só precisamos fazer as perguntas certas e achar as respostas adequadas, então espero que você guarde mais essa informação no seu repertório de soluções.

Abraços

Luciano Caixeta Moreira - {Luti}
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm
www.srnimbus.com.br