Pressione enter para ver os resultados ou esc para cancelar.

Examinando as queries geradas pelo Entity Framework 7

Alguns problemas nos fazem questionar a validade da query gerada por ferramentas ORM. Select n+1 e o uso excessivo de joins são dois casos em que é interessante lembrar que existe um banco de dados por trás e investigar a query gerada. Aí começa a hora do horror. Uma query simples torna-se impossível de debugar. Veja esse simples exemplo:

db.Blog.Where(b => b.BlogId == id).OrderBy(b => b.Url)

Essa consulta inofensiva vira isso no Entity Framework 6:

SELECT
    [Project1].[BlogId] AS [BlogId],
    [Project1].[Url] AS [Url]
    FROM ( SELECT
        [Extent1].[BlogId] AS [BlogId],
        [Extent1].[Url] AS [Url]
        FROM [dbo].[Blog] AS [Extent1]
        WHERE [Extent1].[BlogId] = @p__linq__0
    )  AS [Project1]
    ORDER BY [Project1].[Url] ASC

wut
O que aconteceu aqui? Um Where com OrderBy, sub-queries? Tudo bem, neste caso o execution plan acaba sendo o mesmo, mas eu fiquei tão encucado com isso há um tempo atrás que acreditei estar fazendo algo errado. Parece que eu não estava. O Entity Framework realmente gera queries bizarras, e não tem muito o que você possa fazer.

Uma luz no fim do túnel

(Quase) toda segunda-feira, o Scott Hanselman organiza uma breve sessão de perguntas e respostas no Google Hangouts abordando principalmente as novas tecnologias do ASP.NET vNext (inclusive você pode participar!). Nessas sessões estão diversos membros das equipes de desenvolvimento da Microsoft, no dia 23 de Setembro o tema era Entity Framework 7, e, apesar dos assuntos interessantes que surgiram durante a transmissão, o que mais me chamou a atenção foi uma série de perguntas que um usuário fez, que foram respondidas pelo próprio Rowan Miller – Program Manager da equipe de EF.


(Para assistir ao vídeo da sessão, clique aqui)

Hmmm, melhorias na geração das queries? Que coincidência, era bem disso que eu estava reclamando.

Entity Framework 7 ao resgate?

Equipado de meu ceticismo, eu decidi validar a afirmação do cara que trabalha com isso e sabe mais do que ninguém. Mas antes eu vou explicar brevemente o que torna essa sétima versão tão especial.
O Entity Framework não começou bem, na verdade ele começou muito mal, a boa notícia é que desde sua concepção o EF passou por diversas evoluções e hoje já é um ORM muito mais otimizado e com funções bem legais (async ♥), claro que ainda existem muitos pontos de melhoria, mas o framework já consegue ser bom o suficiente para ser usado em aplicações enterprise. O preço dessa evolução tortuosa é que seu núcleo está cheio de marcas das más decisões tomadas, e esse seu passado reflete em seu futuro também, por isso a equipe decidiu que para que a próxima versão pudesse realmente evoluir como eles gostariam eles começariam reescrevendo o código do zero, sem ter que carregar nenhuma herança indesejada.
É aí que entra o Entity Framework 7, ele é totalmente novo e já começa com uma série de objetivos que guiam sua criação:

  • Leveza – A nova versão não utilizará mais APIs e padrões antigos, isso tornará sua execução mais leve.
  • Portabilidade – O EF hoje só existe nos projetos .NET completo. Na nova versão um dos objetivos é torná-lo mais portável, isso significa rodar no Mono, Mac, Linux, Windows Phone e outras plataformas.
  • Adaptabilidade – O EF atual foi feito pensando apenas em bancos relacionais. Essa não é mais nossa realidade, por isso o EF7 já possui suporte a Azure Table Storage, Redis, SQLite e InMemory (muito útil para testes) com promessa de muitos outros providers a caminho.

São objetivos ambiciosos, e você pode acompanhar (e contribuir com) o desenvolvimento de tudo no GitHub.

Hora da demo!

Primeiramente eu tive que fazer alguns ajustes para que o EF7 rodasse na minha máquina. (O código que eu usei está no GitHub, caso queira pular essa parte)

Instalando o Entity Framework 7

A página do GitHub deles tem um tutorial ensinando a configurar o EF7. Não é preciso instalar a versão 14 CTP do Visual Studio, eu fiz esses testes no Visual Studio 2013, porém, é necessário utilizar a versão 4.5.1 do .NET Framework. Lembre-se que o EF7 está em beta e não deve, de forma alguma, ser usado em produção, acredite, fazendo esta simples demo eu encontrei dois bugs em funções básicas.
Primeiro, quando ele diz para instalar a versão 2.8.3 do NuGet ele realmente quer que você faça isso. É claro que eu ignorei essa parte, porque vi que o meu NuGet estava na versão 2.8.50926.663.
2014-11-06_23-16-06

Eu não sei explicar o motivo exato dessa inconsistência na versão, mas o meu Visual Studio exibia um erro ao tentar instalar o EF7, pesquisando descobri que era justamente porque eu estava com uma versão desatualizada do NuGet, portanto, instale a versão 2.8.3 disponível aqui e reinicie o VS.
Feito isso, você precisará configurar o feed ~secreto~ da galera do vNext. Nele estão disponíveis builds diários de todas ferramentas que estão sendo feitas focadas na nova versão do .NET Framework.

Vá em Tools > Options > NuGet Package ManagerPackage Sources e adicione o endereço https://www.myget.org/F/aspnetvnext/api/v2/ conforme a imagem abaixo:

Options_2014-11-06_23-35-26

Com essas configurações feitas eu criei um projeto Web MVC apenas para exibir as listas das consultas que eu fiz. Eu estou mesmo interessado nas queries geradas, portanto, tanto faz o tipo do projeto usado, apenas certifique-se de ter selecionado o runtime 4.5.1, do contrário o EF7 exibirá um erro na instalação.

Agora que você tem tudo preparado, instale o EF7 para SqlServer através do nuget. Certifique-se de que o feed correto está selecionado e execute o comando

Install-Package EntityFramework.SqlServer –Pre

2014-11-09_09-19-58

Se você receber o erro ‘EntityFramework’ already has a dependency defined for ‘Ix-Async’, certifique-se de que atualizou o NuGet para a versão 2.8.3

Botando o EF7 para funcionar

Agora que tudo está configurado, você pode utilizar o próprio LocalDB como banco de dados da sua aplicação. Eu utilizei o velho exemplo do Blog -> Posts que a própria página do EF7 utiliza. Crie os seguintes arquivos na pasta Model do seu projeto:

Agora é preciso criar o banco de dados definidos em nossos Models através de Migrations.

  1. Instale o Migration no projeto
  2. Install-Package EntityFramework.Commands -Pre
  3. Habilite o Migration e crie o script inicial
  4. Add-Migration MigrationInicial
  5. Agora execute o script criado
  6. Apply-Migration

A última coisa que eu fiz foi habilitar Log para o EF7, para que eu pudesse comparar. No EF6 já é bem fácil adicionar log, este artigo da Microsoft explica muito bem.
Já para o EF7, eu tive que recorrer ao StackOverflow, pois não achei documentado em nenhum lugar. Eu usei este método bem simples, já que eu queria apenas imprimir as queries:

db.Configuration.LoggerFactory.AddProvider(
                new DiagnosticsLoggerProvider(
                    new SourceSwitch("SourceSwitch", "Verbose"),
                    new ConsoleTraceListener()));

Um dia após a criação deste projeto a equipe do EF retirou esta interface de Log, para seguir o tutorial, pegue a versão 7.0.0-beta2-11524 do EF7.

Agora você está com tudo pronto, vamos às análises!

Investigando as queries

Eu decidi testar 3 queries diferentes. Eu não testei nenhuma query complexa, minha dúvida era justamente como o EF está se comportando em relação às queries que não são difíceis de fazer no SQL, mas que eram difíceis de entender pelo código gerado.

Primeiro caso: Where e Order By

Esta é uma consulta que o EF6 já fazia uma bagunça muito grande para algo tão simples. Esse foi o código executado nas duas versões:

db.Blogs.Where(b => b.BlogId == id).OrderByDescending(b => b.Url).ToList();

Entity Framework 6:

SELECT 
    [Project1].[BlogId] AS [BlogId], 
    [Project1].[Url] AS [Url]
    FROM ( SELECT 
        [Extent1].[BlogId] AS [BlogId], 
        [Extent1].[Url] AS [Url]
        FROM [dbo].[Blog] AS [Extent1]
        WHERE [Extent1].[BlogId] = @p__linq__0
    )  AS [Project1]
    ORDER BY [Project1].[Url] DESC

Como já sabíamos, o EF6 não se sai muito bem nessa consulta tão simples. O problema aqui é o where com o order by. Por algum motivo essa combinação resulta na query acima.

Entity Framework 7:

SELECT [b].[BlogId], 
       [b].[Url]
FROM [Blog] AS [b]
WHERE [b].[BlogId] = @p0
ORDER BY [b].[Url] DESC

Ótimo! Essa seria exatamente a query que eu escreveria na mão para fazer essa consulta no banco de dados. Estou começando a acreditar na afirmação do Rowan Miller.

Segundo caso: Include com Where e Order By

Nessa consulta vamos buscar todos os blogs e seus posts (quando existirem) e ordenar pela URL de forma decrescente. Essa é a consulta feita:

db.Blogs.Include(b => b.Posts).Where(b => b.BlogId == id).OrderByDescending(b => b.Url).ToList();

Essa consulta gerará um Inner Join para trazer as informações dos Posts numa única consulta.
Entity Framework 6:

SELECT 
    [Project1].[BlogId] AS [BlogId], 
    [Project1].[Url] AS [Url], 
    [Project1].[C1] AS [C1], 
    [Project1].[PostId] AS [PostId], 
    [Project1].[Content] AS [Content], 
    [Project1].[Title] AS [Title], 
    [Project1].[BlogId1] AS [BlogId1]
    FROM ( SELECT 
        [Extent1].[BlogId] AS [BlogId], 
        [Extent1].[Url] AS [Url], 
        [Extent2].[PostId] AS [PostId], 
        [Extent2].[Content] AS [Content], 
        [Extent2].[Title] AS [Title], 
        [Extent2].[BlogId] AS [BlogId1], 
        CASE WHEN ([Extent2].[PostId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM  [dbo].[Blog] AS [Extent1]
        LEFT OUTER JOIN [dbo].[Post] AS [Extent2] ON [Extent1].[BlogId] = [Extent2].[BlogId]
        WHERE [Extent1].[BlogId] = @p__linq__0
    )  AS [Project1]
    ORDER BY [Project1].[Url] DESC, [Project1].[BlogId] ASC, [Project1].[C1] ASC

Entity Framework 7:

SELECT [p].[BlogId], 
       [p].[Content], 
       [p].[PostId], 
       [p].[Title]
FROM [Post] AS [p]
INNER JOIN (
    SELECT DISTINCT [b].[Url], [b].[BlogId]
    FROM [Blog] AS [b]
    WHERE [b].[BlogId] = @p0
) AS [b] ON [p].[BlogId] = [b].[BlogId]
ORDER BY [b].[Url] DESC, [b].[BlogId]

Mais uma vez o EF7 gera uma query muito mais simples e fácil de entender, muito próxima da query que você provavelmente escreveria manualmente.

Terceiro caso: Include, Where, Order By e Paginação

Agora vamos executar a mesma consulta acima, porém, utilizando paginação.

db.Blog.Include(b => b.Post).Where(b => b.BlogId == id).OrderByDescending(b => b.Url).Skip(1).Take(10).ToList();

Entity Framework 6:

SELECT 
    [Project2].[BlogId] AS [BlogId], 
    [Project2].[Url] AS [Url], 
    [Project2].[C1] AS [C1], 
    [Project2].[PostId] AS [PostId], 
    [Project2].[Content] AS [Content], 
    [Project2].[Title] AS [Title], 
    [Project2].[BlogId1] AS [BlogId1]
    FROM ( SELECT 
        [Limit1].[BlogId] AS [BlogId], 
        [Limit1].[Url] AS [Url], 
        [Extent2].[PostId] AS [PostId], 
        [Extent2].[Content] AS [Content], 
        [Extent2].[Title] AS [Title], 
        [Extent2].[BlogId] AS [BlogId1], 
        CASE WHEN ([Extent2].[PostId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM   (SELECT TOP (10) [Project1].[BlogId] AS [BlogId], [Project1].[Url] AS [Url]
            FROM ( SELECT [Project1].[BlogId] AS [BlogId], [Project1].[Url] AS [Url], row_number() OVER (ORDER BY [Project1].[Url] DESC) AS [row_number]
                FROM ( SELECT 
                    [Extent1].[BlogId] AS [BlogId], 
                    [Extent1].[Url] AS [Url]
                    FROM [dbo].[Blog] AS [Extent1]
                    WHERE [Extent1].[BlogId] = @p__linq__0
                )  AS [Project1]
            )  AS [Project1]
            WHERE [Project1].[row_number] > 1
            ORDER BY [Project1].[Url] DESC ) AS [Limit1]
        LEFT OUTER JOIN [dbo].[Post] AS [Extent2] ON [Limit1].[BlogId] = [Extent2].[BlogId]
    )  AS [Project2]
    ORDER BY [Project2].[Url] DESC, [Project2].[BlogId] ASC, [Project2].[C1] ASC

Entity Framework 7:

SELECT [p].[BlogId], 
       [p].[Content], 
       [p].[PostId], 
       [p].[Title]
FROM [Post] AS [p]
INNER JOIN (
    SELECT DISTINCT [t0].*
    FROM (
        SELECT [b].[BlogId], [b].[Url]
        FROM [Blog] AS [b]
        ORDER BY [b].[Url] DESC, [b].[BlogId] OFFSET @p0 ROWS FETCH NEXT @p1 ROWS ONLY
    ) AS [t0]
    WHERE [t0].[BlogId] = @p2
) AS [b] ON [p].[BlogId] = [b].[BlogId]
ORDER BY [b].[Url] DESC, [b].[BlogId]

O EF7 não só gera uma query mais simples, como também já faz uso do OFFSET FETCH introduzido no SQL Server 2012 para fazer paginação de forma mais eficiente. Yay!

Conclusão

Ainda é cedo para dizer se o Entity Framework 7 terá uma performance melhor em relação às queries geradas, mas os resultados obtidos rodando uma versão beta do framework são bem animadores. É importante lembrar que ele está muito longe do lançamento, isso ficou bem claro quando eu tive que deixar de fora 2 consultas porque elas lançavam exceção no momento de executar a query gerada.
Por enquanto vou dizer que Rowan realmente não mentiu e vou torcer para que isso não mude.

Share on FacebookTweet about this on TwitterShare on Google+Share on LinkedInEmail this to someone
Comentários

13 Comentários

Marcus

Boenas!
Primeiro parabéns pelo artigo, ficou muito bom, simples e direto!
To usando o EF 7 já desde as primeiras versões betas e uma coisa que eu não consigo ver são as querys que ele gera. Como vc faz para ter acesso a isso?
Valeu!

Mahmoud Ali

Olá Marcus, fico feliz que tenha gostado do post!
O log de queries dele agora funciona no mesmo esquema de Logging do ASP.NET, você vai ter a oportunidade de definir como ele vai logar, mais info aqui: https://ef.readthedocs.io/en/latest/miscellaneous/logging.html

Mas eu também já usei esse profiler pra SQL Server que funcionou perfeitamente com o EF7: https://expressprofiler.codeplex.com/

Mauricio Spido

Muito bom seu artigo, objetivo e claro.
Minha preocupação quanto ao EF é referente ao gerenciamento de memória, infelizmente foi o ponto que pesou quando escolhi o NH, que obviamente na época (2 ano atrás) estava bem mais evoluído.
Em testes que fizemos na época a mesma aplicação em NH estava consumindo em torno de 250/300 MB, me refiro a memória do pool da aplicação no IIS. Fizemos a mesma aplicação em EF e tivemos um susto, foi pra quase 800 MB.
Isso olhando a memória de start da aplicação, com o tempo ambas iam crescendo, mas a do EF crescia exponencialmente. Tanto que o servidor de hospedagem (compartilhado) reiniciava o pool, e com o NH está rodando até hoje e nunca tivemos problemas.

Mahmoud Ali

Olá Mauricio, realmente esse é outro ponto que preocupa no EF. Sei que eles estão preocupados com isso também, afinal, agora o EF precisa rodar em dispositivos móveis (com pouca memória). Não cheguei a ver nenhum anúncio oficial sobre melhora nesse aspecto com comparações, mas tenho esperanças de que vai receber uma atenção especial.

paulo

Jovem, estou tentando usar o log num projeto vnext mas não consigo. tem algum projeto básico que poderia me mandar?

Mahmoud Ali

Paulo, infelizmente como disse, fiz esses testes numa versão muito antiga, o EF7 já evoluiu bem mais depois disso, inclusive a estrutura de log mudou para esta: http://stackoverflow.com/a/27658656/1710624

FÚLVIO

Muito bom, parabéns pelo conteúdo …

Yan

Cara, parabéns pela análise contundente da questão. Ficou muito bem explanada.

Mahmoud Ali

Fico feliz que tenha gostado, Yan!

Priscila Mayumi Sato

Puxa, muito bom o seu artigo, parabéns

Mahmoud Ali

Que bom que você gostou, Priscila. Acompanho o seu blog há um tempo, então esse feedback vindo de você foi especialmente legal, hehe.
Caso esteja interessada, agora estou blogando em outro site, junto com outros amigos de várias áreas, dá uma olhada: http://www.high5devs.com

Gustavo

Olá, muito bom o artigo. Muito bem explicado.
Ótimas melhorias para o ef7.

Bruno Pacola

Cara! Muito bom este artigo!! PARABÉNS!!! Cada etapa muito bem explicada e um raciocínio bastante didático. =)


Deixar um comentário