A questão "qual ORM devo usar" está realmente direcionada para a ponta de um enorme iceberg quando se trata da estratégia geral de acesso a dados e da otimização de desempenho em um aplicativo de larga escala.
Design e Manutenção de Banco de Dados
Esse é, por uma ampla margem, o determinante mais importante da taxa de transferência de um aplicativo ou site orientado a dados, e muitas vezes totalmente ignorado pelos programadores.
Se você não usar técnicas adequadas de normalização, seu site estará condenado. Se você não tiver chaves primárias, quase todas as consultas serão lentas. Se você usar anti-padrões conhecidos, como o uso de tabelas para pares de valor-chave (AKA Entity-Attribute-Value) sem um bom motivo, explodirá o número de leituras e gravações físicas.
Se você não tirar proveito dos recursos que o banco de dados oferece, como compactação de página, FILESTREAM
armazenamento (para dados binários), SPARSE
colunas, hierarchyid
hierarquias e assim por diante (todos os exemplos do SQL Server), você não verá nenhum lugar próximo ao desempenho que você poderia estar vendo.
Você deve começar a se preocupar com sua estratégia de acesso a dados depois de projetar seu banco de dados e se convencer de que é o melhor possível, pelo menos por enquanto.
Carregamento ansioso x preguiçoso
A maioria dos ORMs usava uma técnica chamada carregamento lento para relacionamentos, o que significa que, por padrão, ele carrega uma entidade (linha da tabela) de cada vez e faz uma ida e volta ao banco de dados toda vez que precisar carregar um ou muitos chave) linhas.
Isso não é uma coisa boa ou ruim, mas depende do que realmente será feito com os dados e do quanto você sabe de antemão. Às vezes, o carregamento lento é absolutamente a coisa certa a fazer. O NHibernate, por exemplo, pode decidir não consultar nada e simplesmente gerar um proxy para um ID específico. Se tudo o que você precisa é o próprio ID, por que pedir mais? Por outro lado, se você estiver tentando imprimir uma árvore de cada elemento em uma hierarquia de três níveis, o carregamento lento se tornará uma operação O (N²), o que é extremamente ruim para o desempenho.
Um benefício interessante do uso de "SQL puro" (ou seja, consultas brutas do ADO.NET / procedimentos armazenados) é que basicamente obriga a pensar exatamente sobre quais dados são necessários para exibir uma determinada tela ou página. ORMs e características carregamento lento não impedir você de fazer isso, mas eles não dão-lhe a oportunidade de ser ... bem, preguiçoso , e acidentalmente explodir o número de consultas que você executar. Portanto, você precisa entender os recursos de carregamento rápido dos ORMs e estar sempre atento ao número de consultas que você está enviando ao servidor para qualquer solicitação de página.
Armazenamento em cache
Todos os principais ORMs mantêm um cache de primeiro nível, AKA "cache de identidade", o que significa que, se você solicitar a mesma entidade duas vezes por seu ID, ele não precisará de uma segunda ida e volta e também (se você projetou seu banco de dados corretamente ) oferece a capacidade de usar simultaneidade otimista.
O cache L1 é bastante opaco no L2S e EF, é preciso confiar que está funcionando. NHibernate é mais explícito sobre isso ( Get
/ Load
vs. Query
/ QueryOver
). Ainda assim, desde que você tente consultar por ID o máximo possível, você deve ficar bem aqui. Muitas pessoas esquecem o cache L1 e pesquisam repetidamente a mesma entidade várias vezes além de seu ID (ou seja, um campo de pesquisa). Se você precisar fazer isso, salve o ID ou até a entidade inteira para pesquisas futuras.
Há também um cache de nível 2 ("cache de consulta"). O NHibernate possui esse recurso embutido. O Linq to SQL e o Entity Framework compilaram consultas , o que pode ajudar a reduzir bastante as cargas do servidor de aplicativos compilando a própria expressão de consulta, mas não armazena em cache os dados. A Microsoft parece considerar isso uma preocupação de aplicativo e não de acesso a dados, e esse é um grande ponto fraco do L2S e do EF. Escusado será dizer que também é um ponto fraco do SQL "bruto". Para obter um desempenho realmente bom com basicamente qualquer ORM que não seja o NHibernate, você precisa implementar sua própria fachada de cache.
Há também uma "extensão" de cache L2 para EF4, que é boa , mas não é realmente uma substituição por atacado de um cache no nível do aplicativo.
Número de consultas
Bancos de dados relacionais são baseados em conjuntos de dados. Eles são realmente bons em produzir grandes quantidades de dados em um curto período de tempo, mas não são tão bons em termos de latência de consultas, porque há uma certa quantidade de sobrecarga envolvida em todos os comandos. Um aplicativo bem projetado deve aproveitar os pontos fortes deste DBMS e tentar minimizar o número de consultas e maximizar a quantidade de dados em cada uma.
Agora não estou dizendo para consultar o banco de dados inteiro quando você só precisa de uma linha. O que estou dizendo é que, se você precisa de Customer
, Address
, Phone
, CreditCard
, e Order
linhas, tudo ao mesmo tempo, a fim de servir uma única página, então você deve perguntar para todos eles ao mesmo tempo, não execute cada consulta separadamente. Às vezes é pior do que isso, você verá o código que consulta o mesmo Customer
registro 5 vezes seguidas, primeiro para obter o Id
, então o Name
, então o EmailAddress
, então ... é ridiculamente ineficiente.
Mesmo se você precisar executar várias consultas que funcionem em conjuntos de dados completamente diferentes, geralmente é ainda mais eficiente enviar tudo para o banco de dados como um único "script" e retornar vários conjuntos de resultados. Você está preocupado com a sobrecarga, não com a quantidade total de dados.
Isso pode parecer senso comum, mas geralmente é muito fácil perder o controle de todas as consultas que estão sendo executadas em várias partes do aplicativo; seu provedor de associação consulta as tabelas de usuário / função, sua ação de cabeçalho consulta o carrinho de compras, sua ação de menu consulta a tabela de mapas do site, sua ação da barra lateral consulta a lista de produtos em destaque e, talvez, sua página seja dividida em algumas áreas autônomas consulte as tabelas Histórico do pedido, Exibido recentemente, Categoria e Inventário separadamente e, antes que você perceba, execute 20 consultas antes mesmo de começar a exibir a página. Isso simplesmente destrói o desempenho.
Algumas estruturas - e eu estou pensando principalmente no NHibernate aqui - são incrivelmente inteligentes sobre isso e permitem que você use algo chamado futuros que agrupam consultas inteiras e tentam executá-las todas de uma só vez, no último minuto possível. AFAIK, você está por conta própria se quiser fazer isso com qualquer uma das tecnologias da Microsoft; você precisa integrá-lo à lógica do aplicativo.
Indexação, Predicados e Projeções
Pelo menos 50% dos desenvolvedores com quem falo e até alguns DBAs parecem ter problemas com o conceito de cobertura de índices. Eles pensam: "bem, a Customer.Name
coluna está indexada, então todas as pesquisas que faço no nome devem ser rápidas". Só que não funciona dessa maneira, a menos que o Name
índice cubra a coluna específica que você está procurando. No SQL Server, isso é feito INCLUDE
na CREATE INDEX
declaração.
Se você ingenuamente usar em SELECT *
qualquer lugar - e isso é mais ou menos o que todo ORM fará, a menos que você especifique explicitamente o contrário, usando uma projeção -, o DBMS poderá muito bem optar por ignorar completamente seus índices porque eles contêm colunas não cobertas. Uma projeção significa que, por exemplo, em vez de fazer isso:
from c in db.Customers where c.Name == "John Doe" select c
Você faz isso:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
E esta vontade, para a maioria ORMs modernos, instruí-lo apenas para ir e consultar os Id
e Name
colunas que são presumivelmente abrangidos pelo índice (mas não o Email
, LastActivityDate
ou qualquer outra colunas que aconteceu para ficar lá).
Também é muito fácil eliminar completamente quaisquer benefícios de indexação usando predicados inadequados. Por exemplo:
from c in db.Customers where c.Name.Contains("Doe")
... parece quase idêntico à nossa consulta anterior, mas na verdade resultará em uma tabela completa ou uma varredura de índice, pois é convertida em LIKE '%Doe%'
. Da mesma forma, outra consulta que parece suspeitamente simples é:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
Supondo que você tenha um índice BirthDate
, esse predicado tem uma boa chance de torná-lo completamente inútil. Nosso programador hipotético aqui obviamente tentou criar um tipo de consulta dinâmica ("apenas filtre a data de nascimento se esse parâmetro foi especificado"), mas esse não é o caminho certo para fazê-lo. Escrito assim:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
... agora o mecanismo de banco de dados sabe como parametrizar isso e fazer uma busca de índice. Uma pequena mudança, aparentemente insignificante, na expressão da consulta pode afetar drasticamente o desempenho.
Infelizmente, o LINQ em geral facilita muito a gravação de consultas ruins como essa, porque às vezes os provedores conseguem adivinhar o que você estava tentando fazer e otimizar a consulta, e às vezes não. Então, você acaba com resultados frustrantemente inconsistentes, que teriam sido óbvios (para um DBA experiente, de qualquer maneira) se você tivesse escrito SQL simples e antigo.
Basicamente, tudo se resume ao fato de que você realmente precisa ficar de olho no SQL gerado e nos planos de execução que eles levam, e se você não estiver obtendo os resultados esperados, não tenha medo de ignorar o Camada ORM de vez em quando e codifique manualmente o SQL. Isso vale para qualquer ORM, não apenas para a EF.
Transações e Bloqueio
Você precisa exibir dados atualizados até o milissegundo? Talvez - depende - mas provavelmente não. Infelizmente, o Entity Framework não fornecenolock
, você pode usar apenas READ UNCOMMITTED
no nível da transação (não no nível da tabela). De fato, nenhum dos ORMs é particularmente confiável sobre isso; se você quiser fazer leituras sujas, precisará descer para o nível SQL e escrever consultas ad-hoc ou procedimentos armazenados. Então, o que se resume, novamente, é como é fácil fazer isso dentro da estrutura.
O Entity Framework percorreu um longo caminho a esse respeito - a versão 1 do EF (no .NET 3.5) foi horrível, tornou incrivelmente difícil romper a abstração de "entidades", mas agora você tem ExecuteStoreQuery e Translate , por isso é realmente não é tão ruim. Faça amizade com esses caras porque você os usará bastante.
Há também o problema de bloqueio de gravação e deadlocks e a prática geral de reter bloqueios no banco de dados pelo menor tempo possível. Nesse sentido, a maioria dos ORMs (incluindo o Entity Framework) na verdade tendem a ser melhores que o SQL bruto, porque encapsulam o padrão da unidade de trabalho , que no EF é SaveChanges . Em outras palavras, você pode "inserir" ou "atualizar" ou "excluir" entidades no conteúdo do seu coração, sempre que quiser, seguro de que nenhuma alteração será realmente enviada ao banco de dados até que você confirme a unidade de trabalho.
Observe que um UOW não é análogo a uma transação de longa execução. O UOW ainda usa os recursos de simultaneidade otimistas do ORM e rastreia todas as alterações na memória . Nenhuma única instrução DML é emitida até a confirmação final. Isso mantém os tempos de transação o mais baixo possível. Se você criar seu aplicativo usando SQL bruto, é muito difícil obter esse comportamento adiado.
O que isso significa para a EF especificamente: Torne suas unidades de trabalho o mais grosseiras possível e não as comprometa até que seja absolutamente necessário. Faça isso e você terá uma contenção de bloqueio muito menor do que usaria comandos individuais do ADO.NET em momentos aleatórios.
A EF é excelente para aplicativos de alto tráfego / alto desempenho, assim como qualquer outra estrutura é adequada para aplicativos de alto tráfego / alto desempenho. O que importa é como você o usa. Aqui está uma rápida comparação das estruturas mais populares e quais recursos eles oferecem em termos de desempenho (legenda: N = Não suportado, P = Parcial, Y = sim / suportado):
Como você pode ver, o EF4 (a versão atual) não se sai muito mal, mas provavelmente não é o melhor se o desempenho for sua principal preocupação. O NHibernate é muito mais maduro nessa área e até o Linq to SQL fornece alguns recursos de aprimoramento de desempenho que o EF ainda não fornece. O ADO.NET bruto geralmente é mais rápido em cenários de acesso a dados muito específicos , mas, quando você reúne todas as partes, ele realmente não oferece muitos benefícios importantes que você obtém das várias estruturas.