Maio de 2016
Volume 31 - Número 5
Pontos de Dados - Dapper, Entity Framework e Aplicativos Híbridos
Por Julie Lerman
Você provavelmente notou que escrevi muito sobre o Entity Framework, o ORM (Object Relational Mapper) da Microsoft que tem sido a principal API de acesso a dados .NET desde 2008. Há outros ORMs .NET no mercado, porém uma categoria específica, os micro-ORMs, estão em grande evidência devido ao seu excelente desempenho. O micro-ORM que mais ouço falar é o Dapper. O que finalmente chamou minha atenção o suficiente para gastar algum tempo destrinchando-o recentemente foi a tendência de vários desenvolvedores que relataram ter criado soluções híbridas com EF e Dapper, deixando cada ORM fazer o que ele é melhor dentro de um único aplicativo.
Depois de ler inúmeros artigos e postagens de blog, conversar com desenvolvedores e mexer um pouco no Dapper, quis compartilhar minhas descobertas com vocês, especialmente com aqueles que, como eu, talvez até ouviram falar do Dapper, mas não sabem realmente o que ele é ou como funciona (ou até mesmo por que as pessoas o adoram). Lembre-se que eu estou longe de ser um especialista. Em vez disso, conheço o suficiente para satisfazer minha curiosidade por hora e espero que para despertar seu interesse de ir ainda mais fundo.
Por que o Dapper?
O Dapper tem uma história interessante, tendo surgido de um recurso com o qual você pode estar extremamente familiarizado: Marc Gravell e Sam Saffron criaram o Dapper enquanto trabalhavam no Stack Overflow, resolvendo problemas de desempenho da plataforma. O Stack Overflow é um site com tráfego extremamente alto destinado a apresentar problemas de desempenho. De acordo com a página Sobre o Stack Exchange, o Stack Overflow teve 5,7 bilhões de exibições de página em 2015. Em 2011, Saffron escreveu uma postagem de blog sobre o trabalho que ele e Gravell tinham feito, chamado “How I Learned to Stop Worrying and Write My Own ORM” (“Como parei de me preocupar e criei meu próprio ORM”) (bit.ly/), que explica os problemas de desempenho que o Stack enfrentava na época, decorrentes do seu uso de LINQ to SQL. Ele detalha então por que criar um ORM personalizado, o Dapper, foi a resposta para otimizar o acesso aos dados no Stack Overflow. Cinco anos depois, o Dapper agora é amplamente usado e tem software livre. Gravell, Stack e seu membro de equipe Nick Craver continuam a gerenciar ativamente o projeto em github.com/StackExchange/dapper-dot-net.
Dapper em resumo
O Dapper concentra-se em deixar você exercitar suas habilidades de SQL para construir consultas e comandos como você acha que eles devem ser. É mais próximo do “metal” que um ORM padrão, liberando o esforço de interpretar consultas como LINQ to EF em SQL. O Dapper tem algumas funcionalidades transformacionais interessantes como a capacidade de explodir uma lista transmitida para uma cláusula WHERE IN. Porém, para a maior parte, o SQL que você envia para o Dapper está pronto e as consultas chegam ao banco de dados muito mais rápido. Se você for bom em SQL, poderá ter certeza que estará escrevendo os comandos com melhor desempenho possível. Você precisa criar algum tipo de IDbConnection, tal como um SqlConnection com uma cadeia de conexão conhecida, para executar as consultas. Em seguida, com sua API, o Dapper pode executar as consultas para você e, desde que o esquema da consulta corresponda às propriedades do tipo de destino, instanciar e popular automaticamente objetos com os resultados de consulta. Há outro benefício de desempenho notável aqui: O Dapper armazena com eficácia em cache o mapeamento aprendido, resultando em uma desserialização muito rápida das consultas subsequentes. A classe que popularei, DapperDesigner (mostrada na Figura 1), é definida para gerenciar designers que criam roupas muito elegantes.
Figura 1 classe DapperDesigner
public class DapperDesigner
{
public DapperDesigner() {
Products = new List<Product>();
Clients = new List<Client>();
}
public int Id { get; set; }
public string LabelName { get; set; }
public string Founder { get; set; }
public Dapperness Dapperness { get; set; }
public List<Client> Clients { get; set; }
public List<Product> Products { get; set; }
public ContactInfo ContactInfo { get; set; }
}
O projeto no qual estou executando as consultas tem uma referência para o Dapper, que recupero por meio de NuGet (install-package dapper). Veja aqui uma chamada simples do Dapper para executar uma consulta para todas as linhas na tabela DapperDesigners:
var designers = sqlConn.Query<DapperDesigner>("select * from DapperDesigners");
Observe que, para listagens de código deste artigo, estou usando * em vez de projetar explicitamente as colunas para as consultas quando desejo obter todas as colunas de uma tabela. sqlConn é um objeto SqlConnection existente que já instanciei, justamente com sua cadeia de conexão, mas que ainda não abri.
O método Query é um método de extensão fornecido pelo Dapper. Quando esta linha é executada, o Dapper abre a conexão, cria um DbCommand, executa a consulta exatamente como a criei, instancia um objeto do DapperDesigner para cada linha nos resultados e envia os valores dos resultados da consulta para as propriedades dos objetos. O Dapper pode corresponder os valores do resultado com propriedades por meio de alguns padrões mesmo se os nomes das propriedades não corresponderem aos nomes da coluna e mesmo se as propriedades não estiverem na mesma ordem que as colunas correspondentes. Ele ainda não consegue ler mentes, por isso não espere que ele descubra mapeamentos que envolvem, por exemplo, diversos valores de cadeia de caracteres em que as ordens ou nomes das colunas e propriedades não estão sincronizados. Realizei alguns experimentos para ver como ele responderia e há também uma configuração global que controla como o Dapper pode deduzir mapeamentos.
Dapper e consultas relacionais
Meu tipo de DapperDesigner tem várias relações. Há um para muitos (com Products), um para um (ContactInfo) e muitos para muitos (Clients). Testei executar consultas entre essas relações e o Dapper conseguiu lidar com elas. Definitivamente não é fácil expressar uma consulta LINQ to EF com um método Include ou até mesmo uma projeção. Minhas habilidades de TSQL foram levadas ao limite, contudo, pois o EF fez com que eu ficasse indolente nos últimos anos.
Este é um exemplo de consultas em uma relação um para muitos usando o SQL que eu usaria direto no banco de dados:
var sql = @"select * from DapperDesigners D
JOIN Products P
ON P.DapperDesignerId = D.Id";
var designers= conn.Query<DapperDesigner, Product,DapperDesigner>
(sql,(designer, product) => { designer.Products.Add(product);
return designer; });
Observe que o método Query exige que eu especifique ambos os tipos que devem ser construídos, além de indicar o tipo a ser retornado, expresso pelo parâmetro de tipo final (DapperDesigner). Eu uso um lambda de várias linhas para construir primeiro os gráficos, adicionando os produtos relevantes aos objetos de designer pai e então retorno cada designer ao IEnumerable retornado pelo método Query.
A desvantagem de fazer isso dando o meu melhor no SQL é que os resultados são simples, como se eles fossem gerados com o método Include do EF. Obterei uma linha por produto com os designer duplicados. O Dapper tem um método MultiQuery que pode retornar vários conjuntos de resultados. Combinado com o GridReader do Dapper, o desempenho dessas consultas definitivamente supera o do Include do EF.
Mais difícil de codificar, mas mais rápido de executar
Expressar SQL e popular objetos relacionados são tarefas que deixei o EF cuidar em segundo plano, por isso escrever o código definitivamente é um esforço maior. Porém, se você estiver trabalhando com muitos dados e o desempenho do tempo de execução for importante, esse esforço com certeza pode valer a pena. Tenho cerca de 30 mil designer no meu banco de dados de exemplo. Apenas alguns deles têm produtos. Realizei alguns testes de parâmetro de comparação nos quais garanti que estava comparando elementos iguais. Antes de observar os resultados de teste, esses são alguns pontos importantes para entender como eu efetuei essas medições.
Lembre-se que, por padrão, o EF é projetado para rastrear objetos que são resultados de consultas. Isso significa que ele pode criar objetos de rastreamento adicionais, que envolve certo esforço, precisando também interagir com tais objetos de rastreamento. O Dapper, por outro lado, simplesmente despeja os resultados na memória. Por isso é importante retirar o acompanhamento de mudanças do EF do ciclo ao fazer comparações de desempenho. Eu faço isso definindo todas as consultas de EF com o método AsNoTracking. Além disso, ao comparar o desempenho, você precisa aplicar diversos padrões de parâmetro de comparação padrão, tais como aquecer o banco de dados, repetir a consulta muitas vezes e descartar os tempos mais rápido e mais lento. Você pode ver os detalhes de como eu criei meus testes de parâmetro de comparação no download de exemplo. Ainda assim considerei que esses eram testes de parâmetro de comparação “leves”, apenas para dar uma ideia das diferenças. Para parâmetros de comparação mais sérios, você precisaria iterar muito mais vezes que as minhas 25 (começando com 500) e fatorar o desempenho do sistema em execução. Estou executando esses testes em um laptop que usa a instância do LocalDB do SQL Server, por isso meus resultados são úteis apenas para fins de comparação.
Os tempos acompanhados nos meus testes são para execução da consulta e compilação dos resultados. Instancie conexões ou o DbContexts não será contado. O DbContext é reutilizado, por isso o tempo para EF compilar o modelo na memória também não é levado em consideração, pois isso só ocorreria uma vez por instância de aplicativo, não cada consulta.
A Figura 2 mostra os testes “select *” para o Dapper e a consulta EF LINQ para que você possa ver o constructo básico do meu padrão de teste. Observe que, fora da coleta de tempo real, estou coletando o tempo para cada iteração em uma lista (chamada “times”) para análise posterior.
Figura 2 testes para comparar o EF e o Dapper ao consultar todos os DapperDesigners
[TestMethod,TestCategory("EF"),TestCategory("EF,NoTrack")]
public void GetAllDesignersAsNoTracking() {
List<long> times = new List<long>();
for (int i = 0; i < 25; i++) {
using (var context = new DapperDesignerContext()) {
_sw.Reset();
_sw.Start();
var designers = context.Designers.AsNoTracking().ToList();
_sw.Stop();
times.Add(_sw.ElapsedMilliseconds);
_trackedObjects = context.ChangeTracker.Entries().Count();
}
}
var analyzer = new TimeAnalyzer(times);
Assert.IsTrue(true);
}
[TestMethod,TestCategory("Dapper")
public void GetAllDesigners() {
List<long> times = new List<long>();
for (int i = 0; i < 25; i++) {
using (var conn = Utils.CreateOpenConnection()) {
_sw.Reset();
_sw.Start();
var designers = conn.Query<DapperDesigner>("select * from DapperDesigners");
_sw.Stop();
times.Add(_sw.ElapsedMilliseconds);
_retrievedObjects = designers.Count();
}
}
var analyzer = new TimeAnalyzer(times);
Assert.IsTrue(true);
}
Há ainda outro ponto a considerar ao comparar “elementos iguais”. O Dapper recebe SQL bruto. Por padrão, as consultas do EF são expressas com o LINQ to EF e devem passar por algum esforço para compilar o SQL para você. Depois que o SQL é compilado, mesmo SQL que conta com parâmetros, ele é armazenado em cache na memória do aplicativo para que o esforço seja reduzido em caso de repetição. Além disso, o EF tem a capacidade de executar consultas usando SQL bruto, por isso considerei ambas as abordagens. A Figura 3 lista os resultados comparativos dos quatro conjuntos de testes. O download contém ainda mais testes.
Figura 3 Tempo médio em milissegundos para executar uma consulta e popular um objeto com base em 25 iterações, eliminando o mais rápido e o mais lento
Consultas *AsNoTracking | Relação | LINQ ao EF* | SQL bruto do EF* | SQL bruto do Dapper |
Todos os designers (30 mil linhas) | – | 96 | 98 | 77 |
Todos os designers com produtos (30 mil linhas) | 1 : * | 251 | 107 | 91 |
Todos os designers com clientes (30 mil linhas) | * : * | 255 | 106 | 63 |
Todos os designers com contato (30 mil linhas) | 1 : 1 | 322 | 122 | 116 |
Nos cenários mostrados na Figura 3, é fácil argumentar pelo uso do Dapper em vez de LINQ to Entities. Porém as pequenas diferenças entre as consultas de SQL bruto podem nem sempre justificar a mudança para o Dapper para tarefas específicas em um sistema em que você estaria usando o EF. Naturalmente, suas próprias necessidades podem ser diferentes e poderiam afetar o grau de variação entre consulta do EF e do Dapper. Contudo, em um sistema de alto tráfego como o Stack Overflow, mesmo os poucos milissegundos economizados por consulta podem ser cruciais.
Dapper e EF para outras necessidades de persistência
Até o momento, medi consultas simples nas quais apenas obtive todas as colunas de uma tabela com as propriedades exatas correspondentes dos tipos sendo retornadas. E se você estiver projetando consultas em tipos? Desde que o esquema dos resultados corresponda ao tipo, o Dapper não vê diferença na criação dos objetos. O EF, contudo, precisa trabalhar mais se os resultados da projeção não estiverem alinhados com um tipo que faça parte do modelo.
O DapperDesignerContext tem um DbSet para o tipo DapperDesigner. Eu tenho outro tipo no meu sistema chamado MiniDesigner que tem um subconjunto de propriedades do DapperDesigner:
public class MiniDesigner {
public int Id { get; set; }
public string Name { get; set; }
public string FoundedBy { get; set; }
}
O MiniDesigner não faz parte do meu modelo de dados do EF, por isso o DapperDesignerContext não tem qualquer conhecimento deste tipo. Descobri que consultar todas as 30 mil linhas e projetar 30 mil objetos MiniDesigner foi 25% mais rápido com o Dapper do que com o EF usando SQL bruto. Novamente, recomendo criar seu próprio perfil de desempenho e tomar decisões para seu próprio sistema.
O Dapper também pode ser usado para enviar por push dados para o banco de dados com métodos que permitem identificar quais propriedades devem ser usadas para os parâmetros especificados pelo comando, seja usando um comando INSERT ou UPDATE bruto ou executando uma função ou procedimento armazenado no banco de dados. Eu não realizei nenhuma comparação de desempenho para essas tarefas.
Híbrido de Dapper mais EF no mundo real
Há muitíssimos sistemas que usam o Dapper para 100% da sua persistência de dados. Porém lembre-se que o que chamou minha atenção foram os desenvolvedores falando sobre soluções híbridas. Em alguns casos, há sistemas que têm o EF implantado e estão buscando ajustar algumas áreas específicas que apresentam problemas. Em outros, as equipes optaram por usar o Dapper para todas as consultas e o EF para todos os salvamentos.
Em resposta às perguntas que fiz sobre isso no Twitter, recebi comentários variados.
@garypochron me disse que sua equipe estava “avançando usando o Dapper em áreas de muitas chamadas e usando os arquivos de recurso para manter a organização do SQL”. Fiquei surpreso em saber que Simon Hughes (@s1monhughes), autor do popular EF Reverse POCO Generator, segue na direção oposta, usando o Dapper como padrão e recorrendo ao EF para problemas complicados. Ele contou: “uso o Dapper onde for possível. Se for uma atualização complexa, uso o EF”.
Também observei uma variedade de discussões em que a abordagem híbrida foi impulsionada pela separação das preocupações em vez de aprimorar o desempenho. A mais comum delas aproveita a confiabilidade padrão da Identidade do ASP.NET no EF e então usa o Dapper para o resto da persistência na solução.
Trabalhar de forma mais direta com o banco de dados apresenta outra vantagem além do desempenho. Rob Sullivan (@datachomp) e Mike Campbell (@angrypets), ambos especialistas no SQL Server, adoram o Dapper. Rob mencionou que você pode aproveitar as funcionalidades de banco de dados às quais o EF não tem acesso, como pesquisa de texto completo. A longo prazo, este recurso específico é voltado para o desempenho.
Por outro lado, há coisas que você pode fazer com o EF que não são possíveis no Dapper além do acompanhamento de mudanças. Um bom exemplo é um que aproveitei ao compilar a solução que criei para este artigo: a capacidade de migrar seu banco de dados à medida que o modelo muda usando o EF Code First Migrations.
Contudo, o Dapper não é ideal para todo mundo. @damiangray me contou que o Dapper não é uma opção para sua solução porque ele precisa ser capaz de retornar IQueryables de uma parte do seu sistema para outra, não os dados reais. Este tópico, execução de consulta adiada, foi mencionado no repositório do GitHub do Dapper em bit.ly/22CJzJl, se você quiser saber mais sobre isso. Ao desenvolver um sistema híbrido, é um bom caminho usar algum tipo de CQS (Command Query Separation) em que seu design separa modelos para tipos específicos de transações (algo que gosto muito). Desta forma, você não está tentando compilar o código de acesso aos dados cru o suficiente para trabalhar com ambos os EF e o Dapper, o que geralmente resulta em ter que sacrificar os benefícios de cada ORM. Da mesma forma que eu trabalhei neste artigo, Kurt Dowswell publicou uma postagem chamada “Dapper, EF and CQS” (“Dapper, EF e CQS”) (bit.ly/1LEjYvA). Útil para mim, útil para você.
Para aqueles que esperam ansiosamente o CoreCLR e ASP.NET Core, o Dapper evoluiu para incluir suporte para eles também. Você pode encontrar mais informações em um thread no repositório do GitHub do Dapper em bit.ly/1T5m5Ko.
Então, eu finalmente explorei o Dapper. O que eu penso?
E quanto a mim? Eu me arrependo de não ter explorado o Dapper antes e estou feliz de finalmente ter feito isso. Sempre recomendei o AsNoTracking ou usar exibições ou procedimentos no banco de dados para aliviar os problemas de desempenho. Isso nunca falhou comigo ou com meus clientes. Porém, agora sei que tenho outro truque na manga para recomendar aos desenvolvedores que estão interessados em extrair ainda mais desempenho dos seus sistemas que usam EF. Não é um sucesso na certa, como dizemos. Minha recomendação será explorar o Dapper, medir a diferença de desempenho (em escala) e descobrir o equilíbrio entre o desempenho e a facilidade de codificação. Considere o uso óbvio do StackOverflow: consultar perguntas, comentários e respostas e retornar gráficos de uma pergunta com seus comentários e respostas juntamente com alguns metadados (edições) e informações do usuário. Eles estão fazendo os mesmos tipos de consultas e mapeando a mesma forma de resultados diversas vezes. O Dapper foi projetado para brilhar nesse tipo de consulta repetitiva, ficando cada vez mais inteligente e rápido com o passar do tempo. Mesmo se você não tiver um sistema com um número insano de transações para o qual o Dapper foi projetado, você provavelmente verá que uma solução híbrida fornece aquilo que você precisa.
Julie Lermané MVP da Microsoft, mentora e consultora do .NET, e reside nas colinas de Vermont. Você pode encontrá-la em apresentações sobre acesso de dados ou sobre outros tópicos .NET em grupos de usuários e conferências em todo o mundo. Ela escreve no blog thedatafarm.com/blog e é autora do "Programming Entity Framework", bem como de uma edição do Code First e do DbContext, todos da O'Reilly Media. Siga-a no Twitter em @julielerman e confira seus cursos da Pluralsight em juliel.me/PS-Videos.
Agradecemos aos seguintes especialistas técnicos do Stack Overflow pela revisão deste artigo: Nick Craver e Marc Gravell
Nick Craver (@Nick_Craver) é um desenvolvedor, engenheiro de confiabilidade de site e, às vezes, DBA do Stack Overflow. Ele é especializado em ajuste de desempenho em todas as camadas, arquitetura geral do sistema, hardware de data center e manutenção de muitos projetos de software livre como o Opserver. Encontre-o em.
Marc Gravell é um desenvolvedor do Stack Overflow com foco especial em bibliotecas e ferramentas de alto desempenho para .NET, especialmente acesso aos dados, serialização e APIs de rede, contribuindo para uma ampla gama de projetos de software livre nessas áreas.