La domanda "quale ORM dovrei usare" è davvero rivolta alla punta di un enorme iceberg quando si tratta della strategia generale di accesso ai dati e dell'ottimizzazione delle prestazioni in un'applicazione su larga scala.
Progettazione e manutenzione del database
Questo è, in larga misura, il singolo determinante più importante del throughput di un'applicazione o di un sito Web basato sui dati, e spesso viene totalmente ignorato dai programmatori.
Se non usi tecniche di normalizzazione adeguate, il tuo sito è condannato. Se non si dispone di chiavi primarie, quasi ogni query sarà lenta. Se utilizzi anti-pattern noti come l'utilizzo di tabelle per coppie chiave-valore (valore-attributo-entità AKA) senza una buona ragione, esploderai il numero di letture e scritture fisiche.
Se non si sfruttano le funzionalità offerte dal database, come la compressione della pagina, l' FILESTREAM
archiviazione (per i dati binari), le SPARSE
colonne, le hierarchyid
gerarchie e così via (tutti gli esempi di SQL Server), non si vedrà alcun punto vicino al le prestazioni che si potrebbe essere visto.
Dovresti iniziare a preoccuparti della tua strategia di accesso ai dati dopo aver progettato il tuo database e esserti convinto che sia il migliore possibile, almeno per il momento.
Desideroso vs. Caricamento pigro
La maggior parte degli ORM utilizzava una tecnica chiamata caricamento lento per le relazioni, il che significa che per impostazione predefinita caricherà un'entità (riga della tabella) alla volta e farà un giro di andata e ritorno nel database ogni volta che deve caricare uno o più correlati (esterni chiave) righe.
Questa non è una cosa buona o cattiva, piuttosto dipende da cosa verrà effettivamente fatto con i dati e da quanto sai in anticipo. A volte il caricamento lento è assolutamente la cosa giusta da fare. NHibernate, ad esempio, può decidere di non eseguire alcuna query e di generare semplicemente un proxy per un determinato ID. Se tutto ciò di cui hai bisogno è l'ID stesso, perché dovrebbe chiedere di più? D'altra parte, se si sta tentando di stampare un albero di ogni singolo elemento in una gerarchia a 3 livelli, il caricamento lento diventa un'operazione O (N²), che è estremamente dannosa per le prestazioni.
Un vantaggio interessante dell'utilizzo di "SQL puro" (ovvero query ADO.NET non elaborate / stored procedure) è che in pratica ti costringe a pensare esattamente a quali dati sono necessari per visualizzare una determinata schermata o pagina. ORM e le caratteristiche pigro caricamento non impediscono dal fare questo, ma non ti danno la possibilità di essere ... beh, pigro , e accidentalmente esplodere il numero di query che si esegue. Quindi è necessario comprendere le funzionalità di caricamento entusiasta degli ORM ed essere sempre vigili sul numero di query che si inviano al server per una determinata richiesta di pagina.
caching
Tutti i principali ORM mantengono una cache di primo livello, la "cache delle identità" di AKA, il che significa che se si richiede la stessa entità due volte con il suo ID, non è necessario un secondo round-trip e anche (se il database è stato progettato correttamente ) ti dà la possibilità di utilizzare la concorrenza ottimistica.
La cache L1 è piuttosto opaca in L2S ed EF, devi fidarti che funzioni. NHibernate è più esplicito al riguardo ( Get
/ Load
vs. Query
/ QueryOver
). Tuttavia, finché cerchi di eseguire una query per ID il più possibile, dovresti andare bene qui. Molte persone dimenticano la cache L1 e cercano ripetutamente la stessa entità più e più volte da qualcosa di diverso dal suo ID (ad esempio un campo di ricerca). Se è necessario eseguire questa operazione, è necessario salvare l'ID o persino l'intera entità per ricerche future.
C'è anche una cache di livello 2 ("query cache"). NHibernate ha questo built-in. Linq to SQL ed Entity Framework hanno compilato query , che possono aiutare a ridurre un po 'il carico del server delle app compilando l'espressione della query stessa, ma non memorizza nella cache i dati. Microsoft sembra considerarla una preoccupazione dell'applicazione piuttosto che una questione di accesso ai dati, e questo è un grave punto debole sia di L2S che di EF. Inutile dire che è anche un punto debole dell'SQL "grezzo". Per ottenere prestazioni davvero buone praticamente con qualsiasi ORM diverso da NHibernate, è necessario implementare la propria facciata cache.
C'è anche una "estensione" cache L2 per EF4 che va bene , ma in realtà non è un sostituto all'ingrosso per una cache a livello di applicazione.
Numero di query
I database relazionali si basano su set di dati. Sono davvero bravi a produrre grandi quantità di dati in un breve lasso di tempo, ma non sono altrettanto buoni in termini di latenza delle query perché c'è un certo sovraccarico in ogni comando. Un'app ben progettata dovrebbe sfruttare i punti di forza di questo DBMS e cercare di ridurre al minimo il numero di query e massimizzare la quantità di dati in ciascuna.
Ora non sto dicendo di interrogare l'intero database quando hai solo bisogno di una riga. Quello che sto dicendo è che, se avete bisogno di Customer
, Address
, Phone
, CreditCard
, e Order
le righe tutti allo stesso tempo, al fine di servire una singola pagina, allora si dovrebbe chiedere per tutti loro, allo stesso tempo, non si esegue ogni query separatamente. A volte è peggio di così, vedrai il codice che interroga lo stesso Customer
record 5 volte di seguito, prima per ottenere il Id
, poi il Name
, poi il EmailAddress
, quindi ... è ridicolmente inefficiente.
Anche se è necessario eseguire diverse query che funzionano tutte su insiemi di dati completamente diversi, in genere è ancora più efficiente inviare tutto al database come un unico "script" e restituire più insiemi di risultati. È l'overhead che ti interessa, non la quantità totale di dati.
Questo potrebbe sembrare un senso comune, ma spesso è davvero facile perdere traccia di tutte le query che vengono eseguite in varie parti dell'applicazione; il tuo provider di appartenenze interroga le tabelle utente / ruolo, l'azione dell'intestazione interroga il carrello, l'azione del menu interroga la tabella della mappa del sito, l'azione della barra laterale interroga l'elenco dei prodotti in primo piano e quindi forse la tua pagina è divisa in alcune aree autonome separate che interroga separatamente le tabelle Cronologia ordini, Visualizzati di recente, Categoria e Inventario e, prima di conoscerlo, stai eseguendo 20 query prima ancora di poter iniziare a pubblicare la pagina. Distrugge completamente le prestazioni.
Alcuni framework - e sto pensando principalmente a NHibernate qui - sono incredibilmente intelligenti su questo e ti permettono di usare qualcosa chiamato futures che raggruppa intere query e cerca di eseguirle tutte in una volta, all'ultimo minuto possibile. AFAIK, sei da solo se vuoi farlo con una delle tecnologie Microsoft; devi inserirlo nella logica dell'applicazione.
Indicizzazione, predicati e proiezioni
Almeno il 50% degli sviluppatori con cui parlo e persino alcuni DBA sembrano avere problemi con il concetto di copertura degli indici. Pensano "bene, la Customer.Name
colonna è indicizzata, quindi ogni ricerca che faccio sul nome dovrebbe essere veloce". Solo che non funziona in questo modo a meno che l' Name
indice non copra la colonna specifica che stai cercando. In SQL Server, che è fatto con INCLUDE
la CREATE INDEX
dichiarazione.
Se usi ingenuamente SELECT *
ovunque - e questo è più o meno ciò che farà ogni ORM a meno che tu non specifichi esplicitamente diversamente usando una proiezione - allora il DBMS potrebbe benissimo scegliere di ignorare completamente i tuoi indici perché contengono colonne non coperte. Una proiezione significa che, ad esempio, invece di farlo:
from c in db.Customers where c.Name == "John Doe" select c
Fai invece questo:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
E questa volontà, per la maggior parte ORM moderni, istruzioni al fine di andare solo ed interrogare le Id
e Name
colonne, che sono presumibilmente coperti dall'indice (ma non il Email
, LastActivityDate
o qualsiasi altro colonne ti è capitato di attaccare in là).
È anche molto semplice eliminare completamente eventuali vantaggi dell'indicizzazione utilizzando predicati inappropriati. Per esempio:
from c in db.Customers where c.Name.Contains("Doe")
... sembra quasi identico alla nostra query precedente ma in realtà si tradurrà in una scansione completa di tabella o indice perché si traduce in LIKE '%Doe%'
. Allo stesso modo, un'altra query che sembra sospettosamente semplice è:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
Supponendo di avere un indice attivo BirthDate
, questo predicato ha buone probabilità di renderlo completamente inutile. Il nostro ipotetico programmatore qui ha ovviamente tentato di creare una specie di query dinamica ("filtra la data di nascita solo se quel parametro è stato specificato"), ma questo non è il modo giusto per farlo. Scritto in questo modo invece:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
... ora il motore DB sa come parametrizzare questo e fare una ricerca di indice. Una modifica minore, apparentemente insignificante, all'espressione della query può influire drasticamente sulle prestazioni.
Sfortunatamente LINQ in generale rende fin troppo facile scrivere query cattive come questa perché a volte i provider sono in grado di indovinare cosa stavi cercando di fare e ottimizzare la query, a volte no. Quindi alla fine si ottengono risultati frustranti incoerenti che sarebbero stati palesemente ovvi (per un DBA esperto, comunque) se avessi appena scritto un semplice vecchio SQL.
Fondamentalmente tutto si riduce al fatto che devi davvero tenere d'occhio sia l'SQL generato sia i piani di esecuzione che portano a, e se non stai ottenendo i risultati che ti aspetti, non aver paura di bypassare il Livello ORM di tanto in tanto e codice SQL manuale. Questo vale per qualsiasi ORM, non solo per EF.
Transazioni e blocco
Devi visualizzare i dati attuali fino al millisecondo? Forse - dipende - ma probabilmente no. Purtroppo, Entity Framework non ti dànolock
, puoi usarlo solo READ UNCOMMITTED
a livello di transazione (non a livello di tabella). In effetti nessuno degli ORM è particolarmente affidabile al riguardo; se si desidera eseguire letture sporche, è necessario passare al livello SQL e scrivere query ad hoc o procedure memorizzate. Quindi ciò che si riduce a, ancora una volta, è quanto è facile per te farlo nel quadro.
Entity Framework ha fatto molta strada in questo senso - la versione 1 di EF (in .NET 3.5) era terribile, ha reso incredibilmente difficile superare l'astrazione delle "entità", ma ora hai ExecuteStoreQuery e Translate , quindi è davvero non male. Fai amicizia con questi ragazzi perché li userai molto.
C'è anche il problema del blocco della scrittura e dei deadlock e la pratica generale di tenere i blocchi nel database il meno tempo possibile. A questo proposito, la maggior parte degli ORM (incluso Entity Framework) tende ad essere migliore dell'SQL grezzo perché incapsulano l' unità del modello di lavoro , che in EF è SaveChanges . In altre parole, puoi "inserire" o "aggiornare" o "eliminare" entità nel contenuto del tuo cuore, quando vuoi, assicurandoti che nessuna modifica verrà effettivamente trasferita al database fino a quando non commetti l'unità di lavoro.
Si noti che un UOW non è analogo a una transazione di lunga durata. UOW utilizza ancora le funzionalità di concorrenza ottimistica dell'ORM e tiene traccia di tutte le modifiche in memoria . Non viene emessa una singola istruzione DML fino al commit finale. Ciò consente di ridurre al minimo i tempi di transazione. Se si sviluppa l'applicazione utilizzando SQL non elaborato, è abbastanza difficile ottenere questo comportamento differito.
Cosa significa questo per EF: Rendi le tue unità di lavoro il più grossolane possibile e non impegnarle fino a quando non ne avrai assolutamente bisogno. Fai questo e finirai con una contesa di blocco molto più bassa di quella che utilizzeresti i singoli comandi ADO.NET in momenti casuali.
EF va benissimo per le applicazioni ad alto traffico / alte prestazioni, proprio come ogni altro framework va bene per le applicazioni ad alto traffico / alte prestazioni. Ciò che conta è come lo usi. Ecco un rapido confronto tra i framework più popolari e le funzionalità che offrono in termini di prestazioni (legenda: N = non supportato, P = parziale, Y = sì / supportato):
Come puoi vedere, EF4 (la versione attuale) non va troppo male, ma probabilmente non è il massimo se le prestazioni sono la tua preoccupazione principale. NHibernate è molto più maturo in quest'area e anche Linq to SQL offre alcune funzionalità che migliorano le prestazioni che EF non ha ancora. ADO.NET non elaborato sarà spesso più veloce per scenari di accesso ai dati molto specifici , ma, quando si mettono insieme tutti i pezzi, in realtà non offre molti vantaggi importanti che si ottengono dai vari framework.