Pytanie „którego ORM powinienem użyć” naprawdę dotyczy czubka ogromnej góry lodowej, jeśli chodzi o ogólną strategię dostępu do danych i optymalizację wydajności w aplikacji na dużą skalę.
Projektowanie i utrzymanie baz danych
Jest to, z szerokim marginesem, najważniejszy wyznacznik przepustowości aplikacji lub strony internetowej sterowanej danymi i często całkowicie ignorowany przez programistów.
Jeśli nie użyjesz odpowiednich technik normalizacji, Twoja strona jest skazana na niepowodzenie. Jeśli nie masz kluczy podstawowych, prawie każde zapytanie będzie wolne. Jeśli użyjesz dobrze znanych anty-wzorców, takich jak używanie tabel dla par klucz-wartość (AKA Entity-Attribute-Value) bez powodu, rozbijesz liczbę fizycznych odczytów i zapisów.
Jeśli nie skorzystasz z funkcji, które oferuje baza danych, takich jak kompresja strony, FILESTREAM
pamięć masowa (dane binarne), SPARSE
kolumny, hierarchyid
hierarchie itd. (Wszystkie przykłady SQL Server), nie zobaczysz nigdzie w pobliżu wydajność, którą można zobaczyć.
Powinieneś zacząć martwić się strategią dostępu do danych po zaprojektowaniu bazy danych i przekonaniu siebie, że jest ona tak dobra, jak to tylko możliwe, przynajmniej na razie.
Chętni kontra Leniwi Ładowanie
Większość ORM stosowała technikę zwaną leniwym ładowaniem relacji, co oznacza, że domyślnie ładuje jedną jednostkę (wiersz tabeli) na raz i robi objazd do bazy danych za każdym razem, gdy musi załadować jedną lub wiele powiązanych (zagranicznych) klucz) wiersze.
To nie jest dobra ani zła rzecz, raczej zależy to od tego, co faktycznie zrobimy z danymi i od tego, ile wiesz z góry. Czasami leniwe ładowanie jest absolutnie właściwe. Na przykład NHibernate może zdecydować, że w ogóle nie będzie pytać o nic i po prostu wygeneruje serwer proxy dla określonego identyfikatora. Jeśli wszystko, czego kiedykolwiek potrzebujesz, to sam identyfikator, dlaczego miałbyś prosić o więcej? Z drugiej strony, jeśli próbujesz wydrukować drzewo każdego elementu w 3-poziomowej hierarchii, leniwe ładowanie staje się operacją O (N²), co jest bardzo niekorzystne dla wydajności.
Jedną z interesujących korzyści z używania „czystego SQL” (tj. Surowych zapytań / procedur przechowywanych ADO.NET) jest to, że w zasadzie zmusza cię do zastanowienia się, jakie dane są niezbędne do wyświetlenia danego ekranu lub strony. ORMs i cechy leniwy załadunku nie zapobiega cię od robienia tego, ale oni nie dają możliwość bycia ... cóż, ci leniwi i przypadkowego wybuchu liczbę zapytań wykonują. Musisz więc zrozumieć funkcje ładowania ORM i być zawsze czujnym, jeśli chodzi o liczbę zapytań wysyłanych do serwera dla każdego żądania strony.
Buforowanie
Wszystkie główne ORM utrzymują pamięć podręczną pierwszego poziomu, AKA „pamięć podręczną tożsamości”, co oznacza, że jeśli dwukrotnie zażądasz tego samego bytu według jego identyfikatora, nie wymaga to drugiej podróży w obie strony, a także (jeśli poprawnie zaprojektowałeś bazę danych ) daje możliwość korzystania z optymistycznej współbieżności.
Pamięć podręczna L1 jest dość nieprzezroczysta w L2S i EF, musisz w pewien sposób zaufać, że działa. NHibernate mówi o tym bardziej wyraźnie ( Get
/ Load
vs. Query
/ QueryOver
). Tak długo, jak będziesz próbował zapytać według identyfikatora w jak największym stopniu, powinieneś być w porządku. Wiele osób zapomina o pamięci podręcznej L1 i wielokrotnie wyszukuje ten sam byt w kółko za pomocą czegoś innego niż jego identyfikator (tj. Pole odnośnika). Jeśli musisz to zrobić, powinieneś zapisać identyfikator, a nawet cały byt na przyszłe wyszukiwania.
Istnieje również pamięć podręczna poziomu 2 („pamięć podręczna zapytań”). NHibernate ma to wbudowane. Linq do SQL i Entity Framework mają skompilowane zapytania , które mogą pomóc nieco zmniejszyć obciążenia serwera aplikacji, kompilując samo wyrażenie zapytania, ale nie buforuje danych. Wydaje się, że Microsoft uważa to za problem związany z aplikacją, a nie za dostęp do danych, i jest to główny słaby punkt zarówno L2S, jak i EF. Nie trzeba dodawać, że jest to także słaby punkt „surowego” SQL. Aby uzyskać naprawdę dobrą wydajność w zasadzie z dowolnym ORM innym niż NHibernate, musisz wdrożyć własną fasadę buforującą.
Istnieje również „rozszerzenie” pamięci podręcznej L2 dla EF4, co jest w porządku , ale tak naprawdę nie jest hurtowym zamiennikiem pamięci podręcznej na poziomie aplikacji.
Liczba zapytań
Relacyjne bazy danych są oparte na zestawach danych. Są naprawdę dobre w tworzeniu dużych ilości danych w krótkim czasie, ale nie są tak dobre pod względem opóźnienia zapytań, ponieważ każde polecenie wiąże się z pewnym obciążeniem. Dobrze zaprojektowana aplikacja powinna wykorzystać mocne strony tego DBMS i spróbować zminimalizować liczbę zapytań i zmaksymalizować ilość danych w każdym z nich.
Teraz nie mówię, aby przesyłać zapytania do całej bazy danych, gdy potrzebujesz tylko jednego wiersza. Co mówię jest, jeśli potrzebujesz Customer
, Address
, Phone
, CreditCard
i Order
wiersze w tym samym czasie w celu odbycia jedną stronę, to należy poprosić o nich wszystkich w tym samym czasie, nie wykonać każde zapytanie osobno. Czasami jest gorzej, zobaczysz kod, który wysyła kwerendę do tego samego Customer
rekordu 5 razy z rzędu, najpierw, aby uzyskać Id
, a Name
następnie EmailAddress
, a następnie ... to jest absurdalnie nieefektywne.
Nawet jeśli musisz wykonać kilka zapytań, które działają na całkowicie różnych zestawach danych, zwykle bardziej wydajne jest przesłanie ich do bazy danych jako pojedynczego „skryptu” i zwrócenie wielu zestawów wyników. Niepokoi Cię ogólny koszt, a nie całkowita ilość danych.
Może to zabrzmieć jak zdrowy rozsądek, ale często bardzo łatwo jest zgubić wszystkie zapytania wykonywane w różnych częściach aplikacji; Twój dostawca członkostwa pyta tabele użytkowników / ról, twoja akcja Nagłówek pyta o koszyk, twoja akcja Menu pyta o tabelę mapy witryny, twoja akcja na pasku bocznym pyta o listę polecanych produktów, a następnie być może twoja strona jest podzielona na kilka odrębnych autonomicznych obszarów, które przeprowadź osobne zapytania do Tabeli Historii zamówień, Ostatnio oglądane, Kategorii i Zapasów, a zanim się zorientujesz, wykonujesz 20 zapytań, zanim zaczniesz obsługiwać stronę. Po prostu całkowicie niszczy wydajność.
Niektóre frameworki - i myślę tu głównie o NHibernate - są niesamowicie sprytne i pozwalają na użycie czegoś takiego jak futures, które dzielą całe zapytania i próbują wykonać je wszystkie naraz, w ostatniej możliwej chwili. AFAIK, jesteś sam, jeśli chcesz to zrobić za pomocą dowolnej technologii Microsoft; musisz wbudować go w logikę aplikacji.
Indeksowanie, predykaty i prognozy
Przynajmniej 50% deweloperów, z którymi rozmawiam, a nawet niektórzy DBA wydają się mieć problem z koncepcją obejmowania indeksów. Myślą: „cóż, Customer.Name
kolumna jest indeksowana, więc każde wyszukiwanie nazwy powinno być szybkie”. Tyle że to nie działa w ten sposób, chyba że Name
indeks obejmuje konkretną kolumnę, której szukasz. W SQL Server jest to zrobione INCLUDE
w CREATE INDEX
instrukcji.
Jeśli naiwnie używasz SELECT *
wszędzie - i to mniej więcej to, co zrobi każdy ORM, chyba że wyraźnie określisz inaczej za pomocą projekcji - wtedy DBMS może bardzo dobrze zignorować twoje indeksy, ponieważ zawierają nieobjęte kolumny. Projekcja oznacza na przykład, że zamiast tego:
from c in db.Customers where c.Name == "John Doe" select c
Robisz to zamiast tego:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
I będzie to dla większości nowoczesnych ORMs, instruować go tylko iść i kwerendy Id
i Name
kolumn, które są przypuszczalnie objętych indeksem (ale nie Email
, LastActivityDate
lub jakikolwiek inny kolumny zdarzyło się trzymać tam).
Bardzo łatwo jest również całkowicie wyeliminować wszelkie korzyści związane z indeksowaniem przy użyciu nieodpowiednich predykatów. Na przykład:
from c in db.Customers where c.Name.Contains("Doe")
... wygląda prawie identycznie jak nasze poprzednie zapytanie, ale w rzeczywistości spowoduje pełne skanowanie tabeli lub indeksu, ponieważ się tłumaczy LIKE '%Doe%'
. Podobnie inne zapytanie, które wygląda podejrzanie prosto, to:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
Zakładając, że masz indeks BirthDate
, ten predykat ma dużą szansę, aby uczynić go całkowicie bezużytecznym. Nasz hipotetyczny programista najwyraźniej próbował stworzyć coś w rodzaju dynamicznego zapytania („filtruj datę urodzenia tylko, jeśli określono ten parametr”), ale nie jest to właściwy sposób, aby to zrobić. Zamiast tego napisane w ten sposób:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
... teraz silnik DB wie, jak to sparametryzować i przeprowadzić wyszukiwanie indeksu. Jedna niewielka, pozornie nieznaczna zmiana w wyrażeniu zapytania może drastycznie wpłynąć na wydajność.
Niestety, LINQ ogólnie sprawia, że pisanie złych zapytań jest zbyt łatwe, ponieważ czasami dostawcy są w stanie odgadnąć, co próbowaliście zrobić, i zoptymalizować zapytanie, a czasem nie. W efekcie powstają frustrująco niespójne wyniki, które byłyby oślepiająco oczywiste (w każdym razie dla doświadczonego DBA), gdybyś właśnie napisał zwykły stary SQL.
Zasadniczo wszystko sprowadza się do tego, że naprawdę musisz uważnie obserwować zarówno wygenerowany SQL, jak i plany wykonania, do których prowadzą, a jeśli nie osiągniesz oczekiwanych rezultatów, nie bój się ominąć Warstwa ORM raz na jakiś czas i ręcznie koduj SQL. Dotyczy to każdej ORM, nie tylko EF.
Transakcje i blokowanie
Czy potrzebujesz wyświetlać aktualne dane do milisekundy? Może - to zależy - ale prawdopodobnie nie. Niestety, Entity Framework nie dajenolock
, możesz używać tylko READ UNCOMMITTED
na poziomie transakcji (nie na poziomie tabeli). W rzeczywistości żaden z ORM nie jest szczególnie wiarygodny w tym zakresie; jeśli chcesz robić brudne odczyty, musisz zejść do poziomu SQL i pisać zapytania ad-hoc lub procedury składowane. Sprowadza się to do tego, jak łatwo jest to zrobić w ramach.
Entity Framework przeszedł długą drogę w tym względzie - wersja 1 EF (w .NET 3.5) była okropna, sprawiła, że niezwykle trudno było przebić się przez abstrakcję „bytów”, ale teraz masz ExecuteStoreQuery i Tłumacz , więc to naprawdę nieźle. Zaprzyjaźnij się z tymi facetami, ponieważ będziesz ich często używać.
Istnieje również kwestia blokowania zapisu i zakleszczeń oraz ogólnej praktyki trzymania blokad w bazie danych przez jak najkrótszy czas. Pod tym względem większość ORM (w tym Entity Framework) faktycznie jest lepsza niż surowy SQL, ponieważ zawierają one wzorzec jednostki pracy , którym w EF jest SaveChanges . Innymi słowy, możesz „wstawiać” lub „aktualizować” lub „usuwać” byty w treści swojego serca, kiedy tylko chcesz, mając pewność, że żadne zmiany nie zostaną faktycznie wprowadzone do bazy danych, dopóki nie wykonasz jednostki pracy.
Należy pamiętać, że UOW nie jest analogiczny do długotrwałej transakcji. UOW nadal korzysta z optymistycznych funkcji współbieżności ORM i śledzi wszystkie zmiany w pamięci . Do ostatniego zatwierdzenia nie jest emitowana ani jedna instrukcja DML. Dzięki temu czasy transakcji są jak najniższe. Jeśli zbudujesz aplikację przy użyciu surowego SQL, osiągnięcie tego odroczonego zachowania jest dość trudne.
Co to w szczególności oznacza dla EF: spraw, aby twoje jednostki pracy były jak najgrubsze i nie przydzielaj ich, dopóki nie będziesz absolutnie tego potrzebował. Zrób to, a skończysz z znacznie mniejszą rywalizacją o blokadę niż przy użyciu indywidualnych poleceń ADO.NET w przypadkowych momentach.
EF jest całkowicie odpowiedni dla aplikacji o dużym natężeniu ruchu / o wysokiej wydajności, podobnie jak każda inna struktura jest odpowiednia do aplikacji o dużym natężeniu ruchu / o wysokiej wydajności. Liczy się sposób korzystania z niego. Oto szybkie porównanie najpopularniejszych frameworków i ich funkcji pod względem wydajności (legenda: N = nieobsługiwane, P = częściowe, Y = tak / obsługiwane):
Jak widać, EF4 (obecna wersja) nie wypada zbyt źle, ale prawdopodobnie nie jest najlepszy, jeśli wydajność jest twoim głównym zmartwieniem. NHibernate jest znacznie bardziej dojrzały w tym obszarze, a nawet Linq to SQL zapewnia pewne funkcje zwiększające wydajność, których EF jeszcze nie ma. Surowe ADO.NET często będzie szybsze w przypadku bardzo specyficznych scenariuszy dostępu do danych, ale po złożeniu wszystkich elementów tak naprawdę nie oferuje wielu ważnych korzyści, które można uzyskać z różnych platform.