quinta-feira, 28 de julho de 2016

Entity Framework e lazy loading

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 também 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,
 Descricao VARCHAR(500) NOT NULL)
GO

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

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

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

ALTER TABLE dbo.Pessoa
ADD CONSTRAINT fk_Pessoa_Telefone_ID 
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…

    1. Adicione um ADO.NET Entity Data Model (chame do que quiser)
    2. Escolha EF Designer from database
    3. Aponte para seu banco de dados EF (pode manter o nome EFEntities)
    4. Selecione as tabelas Pessoa, Endereco e Telefone
    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 => x.Codigo < 10000);

foreach (var p in r.ToList())
{
    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 => x.Codigo < 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

Nenhum comentário:

Postar um comentário