Die Frage "Welches ORM soll ich verwenden?" Zielt wirklich auf die Spitze eines riesigen Eisbergs ab, wenn es um die allgemeine Datenzugriffsstrategie und die Leistungsoptimierung in einer groß angelegten Anwendung geht.
Datenbankdesign und -pflege
Dies ist mit großem Abstand die wichtigste Determinante für den Durchsatz einer datengesteuerten Anwendung oder Website und wird von Programmierern häufig völlig ignoriert.
Wenn Sie nicht die richtigen Normalisierungstechniken verwenden, ist Ihre Website zum Scheitern verurteilt. Wenn Sie keine Primärschlüssel haben, ist fast jede Abfrage hundeschwach. Wenn Sie bekannte Anti-Patterns verwenden, z. B. Tabellen für Schlüssel-Wert-Paare (AKA Entity-Attribute-Value), explodieren Sie die Anzahl der physischen Lese- und Schreibvorgänge.
Wenn Sie die Funktionen, die Ihnen die Datenbank bietet, wie Seitenkomprimierung, FILESTREAM
Speicher (für Binärdaten), SPARSE
Spalten, hierarchyid
Hierarchien usw. (alle SQL Server-Beispiele) nicht nutzen, werden Sie nicht in der Nähe von sehen Leistung, die Sie sehen könnten .
Sie sollten sich über Ihre Datenzugriffsstrategie Gedanken machen, nachdem Sie Ihre Datenbank entworfen und sich davon überzeugt haben, dass sie zumindest vorerst so gut wie möglich ist.
Eifriges vs. faules Laden
Die meisten ORMs verwendeten eine Technik, die als verzögertes Laden von Beziehungen bezeichnet wird. Dies bedeutet, dass standardmäßig jeweils eine Entität (Tabellenzeile) geladen wird und jedes Mal, wenn eine oder mehrere verwandte (fremde) Entitäten geladen werden müssen, ein Roundtrip zur Datenbank durchgeführt wird Schlüssel) Zeilen.
Dies ist keine gute oder schlechte Sache, sondern hängt vielmehr davon ab, was mit den Daten tatsächlich gemacht wird und wie viel Sie im Vorfeld wissen. Manchmal ist Lazy-Loading genau das Richtige. Beispielsweise kann NHibernate entscheiden, überhaupt nichts abzufragen und einfach einen Proxy für eine bestimmte ID zu generieren . Wenn Sie nur die ID selbst benötigen, warum sollten Sie danach fragen? Wenn Sie dagegen versuchen, einen Baum jedes einzelnen Elements in einer dreistufigen Hierarchie zu drucken, wird das verzögerte Laden zu einer O (N²) -Operation, die für die Leistung äußerst schlecht ist.
Ein interessanter Vorteil der Verwendung von "reinem SQL" (dh ADO.NET-Abfragen / gespeicherten Prozeduren) besteht darin, dass Sie gezwungen sind, genau zu überlegen, welche Daten für die Anzeige eines bestimmten Bildschirms oder einer bestimmten Seite erforderlich sind. ORMs und Funktionen zum verzögerten Laden hindern Sie nicht daran , aber sie bieten Ihnen die Möglichkeit, ... faul zu sein und die Anzahl der von Ihnen ausgeführten Abfragen aus Versehen zu explodieren. Sie müssen also die Funktionen Ihres ORM verstehen, die Sie benötigen, und die Anzahl der Abfragen, die Sie für eine bestimmte Seitenanforderung an den Server senden, stets im Auge behalten.
Caching
Alle wichtigen ORMs verfügen über einen Cache der ersten Ebene, den "Identitätscache" der AKA. Wenn Sie also dieselbe Entität zweimal anhand ihrer ID anfordern, ist kein zweiter Roundtrip erforderlich ) gibt Ihnen die Möglichkeit, optimistische Parallelität zu verwenden.
Der L1-Cache ist in L2S und EF ziemlich undurchsichtig. Man muss sich darauf verlassen, dass er funktioniert. NHibernate geht expliziter vor ( Get
/ Load
vs. Query
/ QueryOver
). Solange Sie versuchen, nach ID zu fragen, sollten Sie hier in Ordnung sein. Viele Leute vergessen den L1-Cache und suchen immer wieder nach derselben Entität, die nicht ihrer ID entspricht (dh nach einem Suchfeld). Wenn Sie dies tun müssen, sollten Sie die ID oder sogar die gesamte Entität für zukünftige Suchvorgänge speichern.
Es gibt auch einen Level 2 Cache ("Query Cache"). NHibernate hat dies eingebaut. Linq to SQL und Entity Framework haben Abfragen kompiliert , wodurch die Auslastung des Anwendungsservers erheblich reduziert werden kann, indem der Abfrageausdruck selbst kompiliert wird, die Daten jedoch nicht zwischengespeichert werden. Microsoft scheint dies eher als Anwendungsproblem als als als Problem des Datenzugriffs zu betrachten, und dies ist eine große Schwachstelle von L2S und EF. Unnötig zu erwähnen, dass dies auch eine Schwachstelle von "rohem" SQL ist. Um mit einem anderen ORM als NHibernate eine wirklich gute Leistung zu erzielen, müssen Sie Ihre eigene Caching-Fassade implementieren.
Es gibt auch eine L2-Cache- "Erweiterung" für EF4, die in Ordnung ist , aber keinen wirklichen Ersatz für einen Cache auf Anwendungsebene darstellt.
Anzahl der Abfragen
Relationale Datenbanken basieren auf Datensätzen . Sie sind wirklich gut auf der Herstellung große Datenmengen in kürzester Zeit, aber sie sind bei weitem nicht so gut in Bezug auf Abfrage Latenz , weil es eine bestimmte Menge an Overhead in jedem Befehl beteiligt. Eine gut gestaltete App sollte die Stärken dieses DBMS nutzen und versuchen, die Anzahl der Abfragen zu minimieren und die Datenmenge in jedem zu maximieren.
Jetzt sage ich nicht, die gesamte Datenbank abzufragen, wenn Sie nur eine Zeile benötigen. Was ich sagen will ist, wenn Sie die Notwendigkeit Customer
, Address
, Phone
, CreditCard
, und Order
Reihen alle zur gleichen Zeit , um eine einzelne Seite zu dienen, dann sollten Sie fragen , für sie alle zur gleichen Zeit, nicht ausführen jeweils getrennt Abfrage. Manchmal ist es schlimmer als das. Sie werden sehen, dass Code Customer
5 Mal hintereinander denselben Datensatz abfragt , um zuerst den Id
, dann den Name
, dann den EmailAddress
, dann ... zu erhalten. Es ist lächerlich ineffizient.
Selbst wenn Sie mehrere Abfragen ausführen müssen, die alle mit völlig unterschiedlichen Datensätzen arbeiten, ist es in der Regel immer noch effizienter, alle Abfragen als einzelnes "Skript" an die Datenbank zu senden und mehrere Ergebnismengen zurückzugeben. Es ist der Aufwand, um den Sie sich kümmern, nicht die Gesamtmenge der Daten.
Das mag sich nach gesundem Menschenverstand anhören, aber es ist oft sehr einfach, den Überblick über alle Abfragen zu verlieren, die in verschiedenen Teilen der Anwendung ausgeführt werden. Ihr Mitgliedschaftsanbieter fragt die Benutzer- / Rollentabellen ab, Ihre Header-Aktion fragt den Einkaufswagen ab, Ihre Menü-Aktion fragt die Site-Map-Tabelle ab, Ihre Sidebar-Aktion fragt die vorgestellte Produktliste ab und dann ist Ihre Seite möglicherweise in einige separate autonome Bereiche unterteilt, die Fragen Sie die Tabellen "Bestellverlauf", "Zuletzt angezeigt", "Kategorie" und "Inventar" separat ab. Bevor Sie dies wissen, führen Sie 20 Abfragen aus, bevor Sie überhaupt mit der Bereitstellung der Seite beginnen können. Es zerstört einfach die Leistung.
Einige Frameworks - und ich denke hier hauptsächlich an NHibernate - sind unglaublich schlau und ermöglichen es Ihnen, so genannte Futures zu verwenden, die ganze Abfragen stapeln und versuchen, sie alle auf einmal in der letzten Minute auszuführen. AFAIK, Sie sind auf sich allein gestellt, wenn Sie dies mit einer der Microsoft-Technologien tun möchten. Sie müssen es in Ihre Anwendungslogik integrieren.
Indizierung, Prädikate und Projektionen
Mindestens 50% der Entwickler, mit denen ich spreche, und sogar einige Datenbankadministratoren scheinen Probleme mit dem Konzept der Indexabdeckung zu haben. Sie denken, "nun, die Customer.Name
Spalte ist indiziert, also sollte jede Suche, die ich nach dem Namen mache, schnell sein." Dies funktioniert jedoch nur, wenn der Name
Index die bestimmte Spalte abdeckt, nach der Sie suchen. In SQL Server ist dies INCLUDE
in der CREATE INDEX
Anweisung erledigt .
Wenn Sie SELECT *
überall naiv verwenden - und dies ist mehr oder weniger das, was jeder ORM tun wird, sofern Sie nicht ausdrücklich etwas anderes mit einer Projektion angeben -, kann das DBMS Ihre Indizes möglicherweise vollständig ignorieren, da sie nicht abgedeckte Spalten enthalten. Eine Projektion bedeutet zum Beispiel, dass stattdessen:
from c in db.Customers where c.Name == "John Doe" select c
Sie tun dies stattdessen:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
Und dies wird für die meisten modernen ORMs, weisen sie nur zu gehen und fragen Sie die Id
und Name
Spalten , die vermutlich durch den Index abgedeckt sind (aber nicht das Email
, LastActivityDate
oder was auch immer andere Spalten , die Sie dort bleiben passiert).
Es ist auch sehr einfach, Indexvorteile durch die Verwendung unangemessener Prädikate vollständig zunichte zu machen. Zum Beispiel:
from c in db.Customers where c.Name.Contains("Doe")
... sieht fast identisch mit unserer vorherigen Abfrage aus, führt jedoch zu einem vollständigen Tabellen- oder Index-Scan, da er in übersetzt wird LIKE '%Doe%'
. Ähnlich ist eine andere Abfrage, die verdächtig einfach aussieht:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
Vorausgesetzt, Sie haben einen Index BirthDate
, hat dieses Prädikat eine gute Chance, ihn völlig unbrauchbar zu machen. Unser hypothetischer Programmierer hier hat offensichtlich versucht, eine Art dynamische Abfrage zu erstellen ("filtern Sie das Geburtsdatum nur, wenn dieser Parameter angegeben wurde"), aber dies ist nicht der richtige Weg, dies zu tun. Stattdessen so geschrieben:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
... jetzt kann die DB-Engine dies parametrieren und eine Indexsuche durchführen. Eine geringfügige, scheinbar unbedeutende Änderung des Abfrageausdrucks kann die Leistung drastisch beeinträchtigen.
Leider macht es LINQ im Allgemeinen allzu einfach, schlechte Abfragen wie diese zu schreiben, da die Anbieter manchmal raten können, was Sie versucht haben, und die Abfrage optimieren können, und manchmal nicht. Am Ende stehen Ihnen frustrierend inkonsistente Ergebnisse zur Verfügung, die für einen erfahrenen Datenbankadministrator (jedenfalls) offensichtlich gewesen wären, wenn Sie nur einfaches altes SQL geschrieben hätten.
Grundsätzlich kommt es darauf an, dass Sie sowohl das generierte SQL als auch die Ausführungspläne, zu denen es führt, genau im Auge behalten müssen. Wenn Sie nicht die erwarteten Ergebnisse erzielen, haben Sie keine Angst, das zu umgehen Von Zeit zu Zeit eine ORM-Schicht erstellen und die SQL von Hand codieren. Dies gilt für jedes ORM, nicht nur für EF.
Transaktionen und Sperren
Müssen Sie Daten anzeigen, die bis zur Millisekunde aktuell sind? Vielleicht - es kommt darauf an - aber wahrscheinlich nicht. Leider bietet Ihnen Entity Framework keine Funktionennolock
, die Sie nur READ UNCOMMITTED
auf Transaktionsebene (nicht auf Tabellenebene) verwenden können. Tatsächlich ist keiner der ORMs in dieser Hinsicht besonders zuverlässig. Wenn Sie Dirty Reads durchführen möchten, müssen Sie sich auf die SQL-Ebene begeben und Ad-hoc-Abfragen oder gespeicherte Prozeduren schreiben. Es kommt also darauf an, wie einfach es für Sie ist, dies im Rahmen zu tun.
Entity Framework hat in dieser Hinsicht einen langen Weg zurückgelegt - Version 1 von EF (in .NET 3.5) war fürchterlich, und es war unglaublich schwierig, die "Entities" -Abstraktion zu durchbrechen, aber jetzt haben Sie ExecuteStoreQuery und Translate , also ist es wirklich nicht so schlecht. Schließe Freundschaften mit diesen Jungs, weil du sie häufig verwendest.
Es gibt auch das Problem der Schreibsperren und Deadlocks sowie die allgemeine Praxis, Sperren in der Datenbank so kurz wie möglich zu halten. In dieser Hinsicht sind die meisten ORMs (einschließlich Entity Framework) in der Regel besser als unformatiertes SQL, da sie die Einheit des Arbeitsmusters kapseln , das in EF SaveChanges ist . Mit anderen Worten, Sie können Entitäten nach Herzenslust "einfügen" oder "aktualisieren" oder "löschen". Dabei können Sie sicher sein, dass keine Änderungen tatsächlich in die Datenbank übertragen werden, bis Sie die Arbeitseinheit festschreiben.
Beachten Sie, dass eine UOW nicht mit einer lang laufenden Transaktion vergleichbar ist. Die UOW verwendet weiterhin die optimistischen Parallelitätsfunktionen des ORM und verfolgt alle Änderungen im Speicher . Bis zum endgültigen Festschreiben wird keine einzige DML-Anweisung ausgegeben. Dies hält die Transaktionszeiten so gering wie möglich. Wenn Sie Ihre Anwendung mit Raw SQL erstellen, ist es ziemlich schwierig, dieses verzögerte Verhalten zu erreichen.
Was dies konkret für EF bedeutet: Machen Sie Ihre Arbeitseinheiten so grob wie möglich und legen Sie sie erst fest, wenn Sie es unbedingt müssen. Wenn Sie dies tun, kommt es zu einer viel geringeren Sperrenkonkurrenz als bei der zufälligen Verwendung einzelner ADO.NET-Befehle.
EF ist für Anwendungen mit hohem Datenverkehr und hoher Leistung vollkommen in Ordnung, genau wie jedes andere Framework für Anwendungen mit hohem Datenverkehr und hoher Leistung. Was zählt, ist, wie Sie es verwenden. Hier ist ein kurzer Vergleich der beliebtesten Frameworks und ihrer Leistungsmerkmale (Legende: N = Nicht unterstützt, P = Teilweise, Y = Ja / Unterstützt):
Wie Sie sehen, schneidet EF4 (die aktuelle Version) nicht allzu schlecht ab, aber es ist wahrscheinlich nicht die beste, wenn die Leistung Ihr Hauptanliegen ist. NHibernate ist in diesem Bereich viel ausgereifter und sogar Linq to SQL bietet einige leistungssteigernde Funktionen, die EF noch nicht bietet. RAW ADO.NET ist für sehr spezielle Datenzugriffsszenarien häufig schneller , aber wenn Sie alle Komponenten zusammenfassen, bietet es nicht wirklich viele wichtige Vorteile, die Sie aus den verschiedenen Frameworks ziehen.