Entity Framework und Performance

    Bei der Arbeit an einem Webportal-Projekt habe ich nach Möglichkeiten gesucht, die Leistung zu verbessern, und bin auf einen kurzen Artikel über Micro-ORM Dapper gestoßen, der von den Autoren des StackOverflow.com-Projekts geschrieben wurde. Ursprünglich wurde das Projekt in Linq2Sql geschrieben, und jetzt wurden alle leistungskritischen Stellen mit der angegebenen Lösung neu geschrieben.
    Der Nachteil dieser und anderer ähnlicher Lösungen, die ich gefunden habe, besteht darin, dass sie im Großen und Ganzen nur zur Materialisierung beitragen und die Arbeit mit ADO.Net verbergen. SQL-Abfragen müssen von Hand geschrieben werden.

    Die Syntax von Linq2Entities verfügt über mehr "sauberen Code", der sowohl das Testen von Code als auch dessen Wiederverwendung ermöglicht. Darüber hinaus erzeugt der Compiler beim Ändern in der Datenbank unmittelbar nach dem Aktualisieren des Kontexts Fehler an allen Stellen, an denen ein entferntes oder umbenanntes Feld verwendet wird. Die geänderte Struktur der Beziehungen zwischen den Tabellen hebt diejenigen Stellen hervor, an denen die entsprechenden Navigationseigenschaften verwendet werden.


    In dem Artikel geht es jedoch nicht darum, wie sehr EF die Entwicklung beschleunigt, und nicht darum, dass es nicht sehr gut ist, einen Teil der Anforderungen in linq zu schreiben, sondern einen Teil direkt in sql. Hier werde ich eine Lösung anbieten, mit der Sie EF-Entities und Linq2Entities-Abfragen einerseits und die "pure Performance" von ADO.Net andererseits kombinieren können. Aber zuerst ein kleiner Hintergrund. Ich glaube, jeder, der mit solchen Projekten gearbeitet hat, war mit der Tatsache konfrontiert, dass Aufrufe pro Zeile sehr langsam funktionieren. Und viele haben wahrscheinlich versucht, dies zu optimieren, indem sie eine große Abfrage geschrieben und alles Mögliche hineingedrückt haben. Es funktioniert, aber es sieht sehr beängstigend aus - der Methodencode ist riesig, schwer zu warten und unmöglich zu testen. Die erste Stufe der Lösung, die ich ausprobiert habe, ist die Materialisierung aller erforderlichen Einheiten mit jeder einzelnen Anforderung.
    Ich werde mit einem Beispiel erklären. Sie müssen eine Liste der Versicherungspolicen anzeigen, die erste Anfrage sieht ungefähr so ​​aus:
    int clientId = 42;
    var policies = context.Set().Where(x => x.active_indicator).Where(x => x.client_id == clientId); 
    

    Um die erforderlichen Informationen anzuzeigen, benötigen wir abhängige oder, wie sie auch genannt werden können, "untergeordnete" Entitäten.
    var coverages = policies.SelectMany(x => x.coverages);
    var premiums = coverages.Select(x => x.premium).Where(x => x.premium_type == SomeIntConstant);
    

    Über NavProps verbundene Entitäten können auch mit Include geladen werden, dies hat jedoch seine eigenen Schwierigkeiten: Es stellte sich heraus, dass es einfacher (und produktiver, dazu später mehr) ist als im oben genannten Beispiel.
    Diese Änderung an sich führte nicht zu einem solchen Leistungsgewinn im Vergleich zur anfänglichen umfassenden Anforderung, sondern vereinfachte den Code, ermöglichte die Aufteilung in kleinere Methoden und machte den Code attraktiver und vertrauter für das Erscheinungsbild.

    Die Leistung kam im nächsten Schritt, als ich den SQL Server-Profiler startete, stellte ich fest, dass zwei von 30 Abfragen 10-15-mal länger waren als die anderen. Die erste dieser Abfragen war wie folgt
    var tasks = workflows.SelectMany(x => x.task)
                         .Where(x => types.Contains(x.task_type))
                         .GroupBy(x => new { x.workflow_id, x.task_type})
                         .Select(x => x.OrderByDescending(y => y.task_id).FirstOrDefault());
    

    Wie sich herausstellte, generiert EF eine sehr erfolglose Anfrage. Nachdem ich GroupBy vom letzten zum ersten Platz verschoben habe, habe ich die Geschwindigkeit dieser Anfragen den anderen näher gebracht und die Gesamtausführungszeit um 30-35% verkürzt.
    var tasks = context.Set
                       .GroupBy(x => new { x.workflow_id, x.task_type})
                       .Select(x => x.OrderByDescending(y => y.task_id).FirstOrDefault())
                       .Join(workflows, task => task.workflow_id, workflow => workflow.workflow_id, (task, workflow) => task)
                       .Where(x => types.Contains(x.task_type));
    

    Nur für den Fall, ich werde sagen, dass Join in dieser Anfrage SelectMany in der vorherigen entspricht.
    Es ist problematisch, einen solchen Fehler in den Tiefen einer großen Anfrage zu finden und zu beseitigen, der fast unmöglich ist. Und durch Include ist dies auch nicht möglich.

    Zurück zum Anfang des Artikels, zu Mikro-ORM, möchte ich sofort sagen, dass ein solcher Ansatz wahrscheinlich nicht in allen Szenarien gerechtfertigt ist. In unserem war es notwendig, einen Teil der Daten aus der Datenbank zu laden, einige Konvertierungen und Berechnungen vorzunehmen und diese über JSON im Browser an den Client zu senden.
    Als Prototyp der Lösung habe ich versucht, die Materialisierung über PetaPoco zu implementieren, und war sehr beeindruckt vom Testergebnis. Der Zeitunterschied bei der Materialisierung der Zielgruppe der Abfragen betrug 4,6x (756 ms gegenüber 3493 ms). Es wäre zwar richtiger zu sagen, dass ich von der Leistung von EF enttäuscht war.
    Aus Gründen strenger Einstellungen in StyleCop funktionierte die Verwendung von PetaPoco im Projekt nicht. Um es an die Aufgabe anzupassen, musste ich mich darauf einlassen und Änderungen vornehmen, sodass die Idee reif war, eine eigene Lösung zu schreiben.
    Die Lösung basiert auf der Tatsache, dass EF beim Generieren von Abfragen in der Abfrage die Feldnamen für das Dataset angibt, die den Eigenschaftsnamen der Objekte entsprechen, die es für den Kontext generiert hat. Alternativ können Sie sich auf die Reihenfolge dieser Felder verlassen, was auch funktioniert. Um die Abfrage und die Parameter aus der Abfrage zu extrahieren, wird die ToObjectQuery-Methode verwendet, und die ToTraceString-Methode und die Parameters-Eigenschaft werden bereits für das resultierende Objekt verwendet. Das Folgende ist ein einfacher Lesezyklus aus MSDN: Materialisierer sind das Highlight der Lösung. PetaPoco gibt den Materializer-Code zur Laufzeit aus, aber ich habe mich entschlossen, mithilfe von T4-Vorlagen Code für sie zu generieren. Ich habe eine Datei zugrunde gelegt, die beim Lesen von .edmx Entitäten für den Kontext generiert, alle Hilfsklassen daraus verwendet und den direkt generierenden Code ersetzt.
    Ein Beispiel für eine generierte Klasse:
        public class currencyMaterialize : IMaterialize, IEqualityComparer
        {
            public currency Materialize(IDataRecord input)
            {
                var output = new currency();        
                output.currency_id = (int)input["currency_id"];
                output.currency_code = input["currency_code"] as string;
                output.currency_name = input["currency_name"] as string;
                return output;
            }
        	public bool Equals(currency x, currency y)
            {
                return x.currency_id == y.currency_id;
            }
            public int GetHashCode(currency obj)
            {
                return obj.currency_id.GetHashCode();
            }
        }
    

    Der von PetaPoco ausgegebene Code ist bedingt identisch mit diesem, was auch durch die gleiche Ausführungszeit bestätigt wird.
    Wie Sie sehen, implementiert die Klasse auch die IEqualityComparer-Schnittstelle, aus der bereits hervorgeht, dass der übliche ReferenceEquals-Vergleich für Objekte, die auf diese Weise materialisiert wurden, im Gegensatz zu den Objekten, die EF materialisiert hat, nicht mehr funktioniert. und brauchte eine solche Ergänzung.

    Ich habe das Ergebnis der Recherche in Form einer Objektvorlage entworfen und in der Visual Studio-Galerie veröffentlicht . Dort finden Sie eine kurze Beschreibung der Verwendung. Ich würde mich freuen, wenn sich jemand für eine Lösung interessiert.

    Jetzt auch beliebt: