Was sind gute Entwurfspraktiken bei der Arbeit mit Entity Framework?


74

Dies gilt hauptsächlich für eine asp.net-Anwendung, bei der nicht über soa auf die Daten zugegriffen wird. Dies bedeutet, dass Sie Zugriff auf die vom Framework geladenen Objekte erhalten, nicht auf Objekte übertragen, obwohl einige Empfehlungen weiterhin gelten.

Dies ist ein Community-Beitrag. Fügen Sie ihn daher nach Belieben hinzu.

Gilt für : Entity Framework 1.0, das mit Visual Studio 2008 sp1 geliefert wird.

Warum überhaupt EF wählen?

Angesichts der Tatsache, dass es sich um eine junge Technologie mit vielen Problemen handelt (siehe unten), kann es schwierig sein, den EF-Zug für Ihr Projekt zu nutzen. Es ist jedoch die Technologie, die Microsoft vorantreibt (auf Kosten von Linq2Sql, einer Teilmenge von EF). Darüber hinaus sind Sie möglicherweise nicht mit NHibernate oder anderen Lösungen zufrieden. Was auch immer die Gründe sein mögen, es gibt Leute da draußen (einschließlich mir), die mit EF arbeiten, und das Leben ist nicht schlecht.

EF und Vererbung

Das erste große Thema ist die Vererbung. EF unterstützt die Zuordnung für geerbte Klassen, die auf zwei Arten beibehalten werden: Tabelle pro Klasse und Tabelle der Hierarchie. Die Modellierung ist einfach und es gibt keine Programmierprobleme mit diesem Teil.

(Das Folgende gilt für Tabellen pro Klassenmodell, da ich keine Erfahrung mit Tabellen pro Hierarchie habe, was ohnehin begrenzt ist.) Das eigentliche Problem tritt auf, wenn Sie versuchen, Abfragen auszuführen, die ein oder mehrere Objekte enthalten, die Teil davon sind Ein Vererbungsbaum: Das generierte SQL ist unglaublich schrecklich, es dauert lange, bis es von der EF analysiert wird, und es dauert auch lange, bis es ausgeführt wird. Dies ist ein echter Show Stopper. Genug, dass EF wahrscheinlich nicht mit Vererbung oder so wenig wie möglich verwendet werden sollte.

Hier ist ein Beispiel, wie schlimm es war. Mein EF-Modell hatte ~ 30 Klassen, von denen ~ 10 Teil eines Vererbungsbaums waren. Beim Ausführen einer Abfrage zum Abrufen eines Elements aus der Basisklasse, etwas so Einfaches wie Base.Get (id), betrug die generierte SQL mehr als 50.000 Zeichen. Wenn Sie dann versuchen, einige Zuordnungen zurückzugeben, degeneriert dies noch mehr und löst SQL-Ausnahmen aus, dass nicht mehr als 256 Tabellen gleichzeitig abgefragt werden können.

Ok, das ist schlecht. Mit dem EF-Konzept können Sie Ihre Objektstruktur ohne (oder mit möglichst wenig) Berücksichtigung der tatsächlichen Datenbankimplementierung Ihrer Tabelle erstellen. Es scheitert dabei völlig.

Also Empfehlungen? Vermeiden Sie Vererbung, wenn Sie können, die Leistung wird so viel besser sein. Verwenden Sie es sparsam, wo Sie müssen. Meiner Meinung nach ist EF damit ein verherrlichtes Tool zur SQL-Generierung für Abfragen, aber die Verwendung bietet immer noch Vorteile. Und Möglichkeiten, Mechanismen zu implementieren, die der Vererbung ähnlich sind.

Vererbung mit Schnittstellen umgehen

Wenn Sie versuchen, eine Art Vererbung mit EF in Gang zu bringen, müssen Sie zunächst wissen, dass Sie einer nicht EF-modellierten Klasse keine Basisklasse zuweisen können. Versuchen Sie es nicht einmal, es wird vom Modellierer überschrieben. Was tun?

Sie können Schnittstellen verwenden, um zu erzwingen, dass Klassen einige Funktionen implementieren. Hier ist beispielsweise eine IEntity-Schnittstelle, mit der Sie Zuordnungen zwischen EF-Entitäten definieren können, bei denen Sie zur Entwurfszeit nicht wissen, um welchen Entitätstyp es sich handelt.

public enum EntityTypes{ Unknown = -1, Dog = 0, Cat }
public interface IEntity
{
    int EntityID { get; }
    string Name { get; }
    Type EntityType { get; }
}
public partial class Dog : IEntity
{
   // implement EntityID and Name which could actually be fields 
   // from your EF model
   Type EntityType{ get{ return EntityTypes.Dog; } }
}

Mit dieser IEntity können Sie dann mit undefinierten Assoziationen in anderen Klassen arbeiten

// lets take a class that you defined in your model.
// that class has a mapping to the columns: PetID, PetType
public partial class Person
{
    public IEntity GetPet()
    {
        return IEntityController.Get(PetID,PetType);
    }
}

welches einige Erweiterungsfunktionen nutzt:

public class IEntityController
{
    static public IEntity Get(int id, EntityTypes type)
    {
        switch (type)
        {
            case EntityTypes.Dog: return Dog.Get(id);
            case EntityTypes.Cat: return Cat.Get(id);
            default: throw new Exception("Invalid EntityType");
        }
    }
}

Nicht so ordentlich wie eine einfache Vererbung, insbesondere wenn man bedenkt, dass man den PetType in einem zusätzlichen Datenbankfeld speichern muss, aber angesichts der Leistungssteigerungen würde ich nicht zurückblicken.

Es kann auch keine Eins-zu-Viele, Viele-zu-Viele-Beziehung modellieren, aber mit kreativen Verwendungen von 'Union' könnte es zum Funktionieren gebracht werden. Schließlich wird der Nebeneffekt des Ladens von Daten in eine Eigenschaft / Funktion des Objekts erzeugt, bei dem Sie vorsichtig sein müssen. Die Verwendung einer klaren Namenskonvention wie GetXYZ () hilft dabei.

Kompilierte Abfragen

Die Leistung von Entity Framework ist nicht so gut wie der direkte Datenbankzugriff mit ADO (offensichtlich) oder Linq2SQL. Es gibt jedoch Möglichkeiten, dies zu verbessern. Eine davon ist das Kompilieren Ihrer Abfragen. Die Leistung einer kompilierten Abfrage ähnelt der von Linq2Sql.

Was ist eine kompilierte Abfrage? Es ist einfach eine Abfrage, für die Sie das Framework anweisen, den analysierten Baum im Speicher zu behalten, damit er beim nächsten Ausführen nicht neu generiert werden muss. Beim nächsten Durchlauf sparen Sie also die Zeit, die zum Parsen des Baums erforderlich ist. Diskontieren Sie dies nicht, da es sich um eine sehr kostspielige Operation handelt, die bei komplexeren Abfragen noch schlimmer wird.

Es gibt zwei Möglichkeiten, eine Abfrage zu kompilieren: Erstellen einer ObjectQuery mit EntitySQL und Verwenden der Funktion CompiledQuery.Compile (). (Beachten Sie, dass Sie durch die Verwendung einer EntityDataSource auf Ihrer Seite tatsächlich ObjectQuery mit EntitySQL verwenden, sodass diese kompiliert und zwischengespeichert wird.)

Nebenbei, falls Sie nicht wissen, was EntitySQL ist. Es ist eine auf Zeichenfolgen basierende Methode zum Schreiben von Abfragen für die EF. Hier ein Beispiel: "Wählen Sie den Wert dog aus Entities.DogSet als Hund aus, wobei dog.ID = @ID". Die Syntax ist der SQL-Syntax ziemlich ähnlich. Sie können auch ziemlich komplexe Objektmanipulationen durchführen, was [hier] [1] gut erklärt wird.

Ok, hier ist, wie es mit ObjectQuery <> gemacht wird

        string query = "select value dog " +
                       "from Entities.DogSet as dog " +
                       "where dog.ID = @ID";

        ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>(query, EntityContext.Instance));
        oQuery.Parameters.Add(new ObjectParameter("ID", id));
        oQuery.EnablePlanCaching = true;
        return oQuery.FirstOrDefault();

Wenn Sie diese Abfrage zum ersten Mal ausführen, generiert das Framework den Ausdrucksbaum und speichert ihn im Speicher. Wenn es das nächste Mal ausgeführt wird, sparen Sie diesen kostspieligen Schritt. In diesem Beispiel ist EnablePlanCaching = true, was nicht erforderlich ist, da dies die Standardoption ist.

Die andere Möglichkeit, eine Abfrage für die spätere Verwendung zu kompilieren, ist die CompiledQuery.Compile-Methode. Dies verwendet einen Delegaten:

    static readonly Func<Entities, int, Dog> query_GetDog =
        CompiledQuery.Compile<Entities, int, Dog>((ctx, id) =>
            ctx.DogSet.FirstOrDefault(it => it.ID == id));

oder mit linq

    static readonly Func<Entities, int, Dog> query_GetDog =
        CompiledQuery.Compile<Entities, int, Dog>((ctx, id) =>
            (from dog in ctx.DogSet where dog.ID == id select dog).FirstOrDefault());

um die Abfrage aufzurufen:

query_GetDog.Invoke( YourContext, id );

Der Vorteil von CompiledQuery besteht darin, dass die Syntax Ihrer Abfrage zur Kompilierungszeit überprüft wird, während dies bei EntitySQL nicht der Fall ist. Es gibt jedoch andere Überlegungen ...

Beinhaltet

Nehmen wir an, Sie möchten, dass die Daten für den Hundebesitzer von der Abfrage zurückgegeben werden, um zu vermeiden, dass zwei Aufrufe an die Datenbank erfolgen. Einfach zu machen, oder?

EntitySQL

        string query = "select value dog " +
                       "from Entities.DogSet as dog " +
                       "where dog.ID = @ID";
        ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>(query, EntityContext.Instance)).Include("Owner");
        oQuery.Parameters.Add(new ObjectParameter("ID", id));
        oQuery.EnablePlanCaching = true;
        return oQuery.FirstOrDefault();

CompiledQuery

    static readonly Func<Entities, int, Dog> query_GetDog =
        CompiledQuery.Compile<Entities, int, Dog>((ctx, id) =>
            (from dog in ctx.DogSet.Include("Owner") where dog.ID == id select dog).FirstOrDefault());

Was ist nun, wenn Sie das Include parametrisieren möchten? Was ich meine ist, dass Sie eine einzelne Get () -Funktion haben möchten, die von verschiedenen Seiten aufgerufen wird, die sich um verschiedene Beziehungen für den Hund kümmern. Einer kümmert sich um den Besitzer, ein anderer um sein Lieblingsessen, ein anderer um sein FavotireToy und so weiter. Grundsätzlich möchten Sie der Abfrage mitteilen, welche Zuordnungen geladen werden sollen.

Mit EntitySQL ist das ganz einfach

public Dog Get(int id, string include)
{
        string query = "select value dog " +
                       "from Entities.DogSet as dog " +
                       "where dog.ID = @ID";

        ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>(query, EntityContext.Instance))
    .IncludeMany(include);
        oQuery.Parameters.Add(new ObjectParameter("ID", id));
        oQuery.EnablePlanCaching = true;
        return oQuery.FirstOrDefault();
}

Das Include verwendet einfach die übergebene Zeichenfolge. Leicht genug. Beachten Sie, dass es möglich ist, die Include-Funktion (Zeichenfolge) (die nur einen einzelnen Pfad akzeptiert) mit einer IncludeMany-Funktion (Zeichenfolge) zu verbessern, mit der Sie eine Zeichenfolge von durch Kommas getrennten Zuordnungen zum Laden übergeben können. Weitere Informationen zu dieser Funktion finden Sie im Erweiterungsabschnitt.

Wenn wir dies jedoch mit CompiledQuery versuchen, stoßen wir auf zahlreiche Probleme:

Das Offensichtliche

    static readonly Func<Entities, int, string, Dog> query_GetDog =
        CompiledQuery.Compile<Entities, int, string, Dog>((ctx, id, include) =>
            (from dog in ctx.DogSet.Include(include) where dog.ID == id select dog).FirstOrDefault());

wird ersticken, wenn aufgerufen mit:

query_GetDog.Invoke( YourContext, id, "Owner,FavoriteFood" );

Weil Include (), wie oben erwähnt, nur einen einzigen Pfad in der Zeichenfolge sehen möchte und hier geben wir 2: "Owner" und "FavoriteFood" (was nicht mit "Owner.FavoriteFood" zu verwechseln ist!).

Verwenden wir dann IncludeMany (), eine Erweiterungsfunktion

    static readonly Func<Entities, int, string, Dog> query_GetDog =
        CompiledQuery.Compile<Entities, int, string, Dog>((ctx, id, include) =>
            (from dog in ctx.DogSet.IncludeMany(include) where dog.ID == id select dog).FirstOrDefault());

Diesmal wieder falsch, weil der EF IncludeMany nicht analysieren kann, weil es nicht Teil der Funktionen ist, die erkannt werden: Es ist eine Erweiterung.

Ok, Sie möchten eine beliebige Anzahl von Pfaden an Ihre Funktion übergeben, und Includes () verwendet nur einen einzigen. Was ist zu tun? Sie können entscheiden, dass Sie niemals mehr als beispielsweise 20 Includes benötigen, und jede einzelne Zeichenfolge in einer Struktur an CompiledQuery übergeben. Aber jetzt sieht die Abfrage so aus:

from dog in ctx.DogSet.Include(include1).Include(include2).Include(include3)
.Include(include4).Include(include5).Include(include6)
.[...].Include(include19).Include(include20) where dog.ID == id select dog

das ist auch schrecklich. Ok, aber warte eine Minute. Können wir mit CompiledQuery keine ObjectQuery <> zurückgeben? Dann setzen Sie die Includes darauf? Nun, das hätte ich auch gedacht:

    static readonly Func<Entities, int, ObjectQuery<Dog>> query_GetDog =
        CompiledQuery.Compile<Entities, int, string, ObjectQuery<Dog>>((ctx, id) =>
            (ObjectQuery<Dog>)(from dog in ctx.DogSet where dog.ID == id select dog));
public Dog GetDog( int id, string include )
{
    ObjectQuery<Dog> oQuery = query_GetDog(id);
    oQuery = oQuery.IncludeMany(include);
    return oQuery.FirstOrDefault;   
}

Das hätte funktionieren sollen, außer dass Sie beim Aufrufen von IncludeMany (oder Include, Where, OrderBy ...) die zwischengespeicherte kompilierte Abfrage ungültig machen, da sie jetzt völlig neu ist! Der Ausdrucksbaum muss also repariert werden, und Sie erhalten erneut einen Leistungseinbruch.

Was ist die Lösung? Sie können CompiledQueries einfach nicht mit parametrisierten Includes verwenden. Verwenden Sie stattdessen EntitySQL. Dies bedeutet nicht, dass CompiledQueries nicht verwendet werden. Es eignet sich hervorragend für lokalisierte Abfragen, die immer im selben Kontext aufgerufen werden. Im Idealfall sollte CompiledQuery immer verwendet werden, da die Syntax beim Kompilieren überprüft wird. Aufgrund von Einschränkungen ist dies jedoch nicht möglich.

Ein Anwendungsbeispiel wäre: Möglicherweise möchten Sie eine Seite haben, auf der abgefragt wird, welche zwei Hunde dasselbe Lieblingsfutter haben. Dies ist für eine BusinessLayer-Funktion etwas eng, sodass Sie es in Ihre Seite einfügen und genau wissen, um welche Art von Includes es sich handelt erforderlich.

Übergabe von mehr als 3 Parametern an eine CompiledQuery

Func ist auf 5 Parameter beschränkt, von denen der letzte der Rückgabetyp und der erste Ihr Entities-Objekt aus dem Modell ist. Sie haben also 3 Parameter. Eine Pitance, die aber sehr leicht verbessert werden kann.

public struct MyParams
{
    public string param1;
    public int param2;
    public DateTime param3;
}

    static readonly Func<Entities, MyParams, IEnumerable<Dog>> query_GetDog =
        CompiledQuery.Compile<Entities, MyParams, IEnumerable<Dog>>((ctx, myParams) =>
            from dog in ctx.DogSet where dog.Age == myParams.param2 && dog.Name == myParams.param1 and dog.BirthDate > myParams.param3 select dog);

public List<Dog> GetSomeDogs( int age, string Name, DateTime birthDate )
{
    MyParams myParams = new MyParams();
    myParams.param1 = name;
    myParams.param2 = age;
    myParams.param3 = birthDate;
    return query_GetDog(YourContext,myParams).ToList();
}

Rückgabetypen (dies gilt nicht für EntitySQL-Abfragen, da sie während der Ausführung nicht gleichzeitig mit der CompiledQuery-Methode kompiliert werden.)

Wenn Sie mit Linq arbeiten, erzwingen Sie normalerweise die Ausführung der Abfrage erst im allerletzten Moment, falls einige andere nachgeschaltete Funktionen die Abfrage auf irgendeine Weise ändern möchten:

    static readonly Func<Entities, int, string, IEnumerable<Dog>> query_GetDog =
        CompiledQuery.Compile<Entities, int, string, IEnumerable<Dog>>((ctx, age, name) =>
            from dog in ctx.DogSet where dog.Age == age && dog.Name == name select dog);

public IEnumerable<Dog> GetSomeDogs( int age, string name )
{
    return query_GetDog(YourContext,age,name);
}
public void DataBindStuff()
{
    IEnumerable<Dog> dogs = GetSomeDogs(4,"Bud");
    // but I want the dogs ordered by BirthDate
    gridView.DataSource = dogs.OrderBy( it => it.BirthDate );

}

Was wird hier passieren? Wenn Sie weiterhin mit der ursprünglichen ObjectQuery spielen (dies ist der tatsächliche Rückgabetyp der Linq-Anweisung, die IEnumerable implementiert), wird die kompilierte Abfrage ungültig und muss erneut analysiert werden. Als Faustregel gilt also, stattdessen eine Liste <> von Objekten zurückzugeben.

    static readonly Func<Entities, int, string, IEnumerable<Dog>> query_GetDog =
        CompiledQuery.Compile<Entities, int, string, IEnumerable<Dog>>((ctx, age, name) =>
            from dog in ctx.DogSet where dog.Age == age && dog.Name == name select dog);

public List<Dog> GetSomeDogs( int age, string name )
{
    return query_GetDog(YourContext,age,name).ToList(); //<== change here
}
public void DataBindStuff()
{
    List<Dog> dogs = GetSomeDogs(4,"Bud");
    // but I want the dogs ordered by BirthDate
    gridView.DataSource = dogs.OrderBy( it => it.BirthDate );

}

Wenn Sie ToList () aufrufen, wird die Abfrage gemäß der kompilierten Abfrage ausgeführt, und später wird OrderBy für die Objekte im Speicher ausgeführt. Es mag etwas langsamer sein, aber ich bin mir nicht mal sicher. Eine sichere Sache ist, dass Sie sich keine Sorgen machen müssen, wenn Sie die ObjectQuery falsch behandeln und den kompilierten Abfrageplan ungültig machen.

Auch dies ist keine pauschale Aussage. ToList () ist ein defensiver Programmiertrick. Wenn Sie jedoch einen gültigen Grund haben, ToList () nicht zu verwenden, fahren Sie fort. Es gibt viele Fälle, in denen Sie die Abfrage vor der Ausführung verfeinern möchten.

Performance

Welche Auswirkungen hat das Kompilieren einer Abfrage auf die Leistung? Es kann tatsächlich ziemlich groß sein. Als Faustregel gilt, dass das Kompilieren und Zwischenspeichern der Abfrage zur Wiederverwendung mindestens doppelt so lange dauert, bis sie einfach ohne Zwischenspeichern ausgeführt wird. Bei komplexen Abfragen (inhärent lesen) habe ich bis zu 10 Sekunden gesehen.

Wenn also eine vorkompilierte Abfrage zum ersten Mal aufgerufen wird, erhalten Sie einen Leistungseinbruch. Nach diesem ersten Treffer ist die Leistung deutlich besser als bei derselben nicht vorkompilierten Abfrage. Praktisch das gleiche wie Linq2Sql

Wenn Sie beim ersten Mal eine Seite mit vorkompilierten Abfragen laden, erhalten Sie einen Treffer. Es wird in vielleicht 5-15 Sekunden geladen (offensichtlich werden mehr als eine vorkompilierte Abfrage aufgerufen), während nachfolgende Ladevorgänge weniger als 300 ms dauern. Dramatischer Unterschied, und es liegt an Ihnen, zu entscheiden, ob es für Ihren ersten Benutzer in Ordnung ist, einen Treffer zu erzielen, oder ob ein Skript Ihre Seiten aufrufen soll, um eine Zusammenstellung der Abfragen zu erzwingen.

Kann diese Abfrage zwischengespeichert werden?

{
    Dog dog = from dog in YourContext.DogSet where dog.ID == id select dog;
}

Nein, Ad-hoc-Linq-Abfragen werden nicht zwischengespeichert, und bei jedem Aufruf fallen die Kosten für die Generierung des Baums an.

Parametrisierte Abfragen

Die meisten Suchfunktionen umfassen stark parametrisierte Abfragen. Es stehen sogar Bibliotheken zur Verfügung, mit denen Sie eine parametrisierte Abfrage aus Lamba-Ausdrücken erstellen können. Das Problem ist, dass Sie mit diesen keine vorkompilierten Abfragen verwenden können. Eine Möglichkeit, dies zu umgehen, besteht darin, alle möglichen Kriterien in der Abfrage zuzuordnen und zu kennzeichnen, welches Sie verwenden möchten:

public struct MyParams
{
    public string name;
public bool checkName;
    public int age;
public bool checkAge;
}

    static readonly Func<Entities, MyParams, IEnumerable<Dog>> query_GetDog =
        CompiledQuery.Compile<Entities, MyParams, IEnumerable<Dog>>((ctx, myParams) =>
            from dog in ctx.DogSet 
    where (myParams.checkAge == true && dog.Age == myParams.age) 
        && (myParams.checkName == true && dog.Name == myParams.name ) 
    select dog);

protected List<Dog> GetSomeDogs()
{
    MyParams myParams = new MyParams();
    myParams.name = "Bud";
    myParams.checkName = true;
    myParams.age = 0;
    myParams.checkAge = false;
    return query_GetDog(YourContext,myParams).ToList();
}

Der Vorteil hierbei ist, dass Sie alle Vorteile eines vorkompilierten Quert erhalten. Die Nachteile sind, dass Sie höchstwahrscheinlich eine where-Klausel erhalten, die ziemlich schwierig zu pflegen ist, dass Sie eine größere Strafe für das Vorkompilieren der Abfrage erhalten und dass jede von Ihnen ausgeführte Abfrage nicht so effizient ist, wie sie sein könnte (insbesondere) mit eingeworfenen Joins).

Eine andere Möglichkeit besteht darin, Stück für Stück eine EntitySQL-Abfrage zu erstellen, wie wir es alle mit SQL getan haben.

protected List<Dod> GetSomeDogs( string name, int age)
{
string query = "select value dog from Entities.DogSet where 1 = 1 ";
    if( !String.IsNullOrEmpty(name) )
        query = query + " and dog.Name == @Name ";
if( age > 0 )
    query = query + " and dog.Age == @Age ";

    ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>( query, YourContext );
    if( !String.IsNullOrEmpty(name) )
        oQuery.Parameters.Add( new ObjectParameter( "Name", name ) );
if( age > 0 )
        oQuery.Parameters.Add( new ObjectParameter( "Age", age ) );

return oQuery.ToList();
}

Hier sind die Probleme: - Während der Kompilierung erfolgt keine Syntaxprüfung. - Jede unterschiedliche Kombination von Parametern generiert eine andere Abfrage, die beim ersten Ausführen vorkompiliert werden muss. In diesem Fall gibt es nur 4 verschiedene mögliche Abfragen (keine Parameter, nur Alter, nur Name und beide Parameter), aber Sie können sehen, dass es bei einer normalen Weltsuche viel mehr geben kann. - Niemand verkettet gerne Strings!

Eine andere Möglichkeit besteht darin, eine große Teilmenge der Daten abzufragen und sie dann im Speicher einzugrenzen. Dies ist besonders nützlich, wenn Sie mit einer bestimmten Teilmenge der Daten arbeiten, wie alle Hunde in einer Stadt. Sie wissen, dass es viele gibt, aber Sie wissen auch, dass es nicht so viele gibt ... sodass Ihre CityDog-Suchseite alle Hunde für die Stadt im Speicher laden kann. Dies ist eine einzelne vorkompilierte Abfrage und verfeinert dann die Ergebnisse

protected List<Dod> GetSomeDogs( string name, int age, string city)
{
string query = "select value dog from Entities.DogSet where dog.Owner.Address.City == @City ";
    ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>( query, YourContext );
    oQuery.Parameters.Add( new ObjectParameter( "City", city ) );

List<Dog> dogs = oQuery.ToList();

if( !String.IsNullOrEmpty(name) )
        dogs = dogs.Where( it => it.Name == name );
if( age > 0 )
        dogs = dogs.Where( it => it.Age == age );

return dogs;
}

Dies ist besonders nützlich, wenn Sie mit der Anzeige aller Daten beginnen und dann das Filtern zulassen.

Probleme: - Kann zu ernsthafter Datenübertragung führen, wenn Sie mit Ihrer Teilmenge nicht vorsichtig sind. - Sie können nur nach den zurückgegebenen Daten filtern. Wenn Sie die Dog.Owner-Zuordnung nicht zurückgeben, können Sie nicht nach dem Dog.Owner.Name filtern. Was ist also die beste Lösung? Es gibt keine. Sie müssen die Lösung auswählen, die für Sie und Ihr Problem am besten geeignet ist: - Verwenden Sie die Lambda-basierte Abfrageerstellung, wenn Sie Ihre Abfragen nicht vorkompilieren möchten. - Verwenden Sie eine vollständig definierte vorkompilierte Linq-Abfrage, wenn Ihre Objektstruktur nicht zu komplex ist. - Verwenden Sie die EntitySQL / String-Verkettung, wenn die Struktur komplex sein kann und wenn die mögliche Anzahl der verschiedenen resultierenden Abfragen gering ist (was weniger Treffer vor der Kompilierung bedeutet).

Singleton-Zugang

Der beste Weg, um mit Ihrem Kontext und Ihren Entitäten auf allen Ihren Seiten umzugehen, ist die Verwendung des Singleton-Musters:

public sealed class YourContext
{
    private const string instanceKey = "On3GoModelKey";

    YourContext(){}

    public static YourEntities Instance
    {
        get
        {
            HttpContext context = HttpContext.Current;
            if( context == null )
                return Nested.instance;

            if (context.Items[instanceKey] == null)
            {
                On3GoEntities entity = new On3GoEntities();
                context.Items[instanceKey] = entity;
            }
            return (YourEntities)context.Items[instanceKey];
        }
    }

    class Nested
    {
        // Explicit static constructor to tell C# compiler
        // not to mark type as beforefieldinit
        static Nested()
        {
        }

        internal static readonly YourEntities instance = new YourEntities();
    }
}

NoTracking, lohnt es sich?

Wenn Sie eine Abfrage ausführen, können Sie dem Framework anweisen, die Objekte zu verfolgen, die zurückgegeben werden sollen oder nicht. Was heißt das? Wenn die Nachverfolgung aktiviert ist (Standardoption), verfolgt das Framework, was mit dem Objekt geschieht (wurde es geändert? Erstellt? Gelöscht?) Und verknüpft Objekte auch miteinander, wenn weitere Abfragen aus der Datenbank durchgeführt werden ist hier von Interesse.

Nehmen wir zum Beispiel an, dass Hund mit ID == 2 einen Besitzer hat, dessen ID == 10 ist.

Dog dog = (from dog in YourContext.DogSet where dog.ID == 2 select dog).FirstOrDefault();
    //dog.OwnerReference.IsLoaded == false;
    Person owner = (from o in YourContext.PersonSet where o.ID == 10 select dog).FirstOrDefault();
    //dog.OwnerReference.IsLoaded == true;

Wenn wir dasselbe ohne Nachverfolgung tun würden, wäre das Ergebnis anders.

ObjectQuery<Dog> oDogQuery = (ObjectQuery<Dog>)
    (from dog in YourContext.DogSet where dog.ID == 2 select dog);
oDogQuery.MergeOption = MergeOption.NoTracking;
Dog dog = oDogQuery.FirstOrDefault();
    //dog.OwnerReference.IsLoaded == false;
ObjectQuery<Person> oPersonQuery = (ObjectQuery<Person>)
    (from o in YourContext.PersonSet where o.ID == 10 select o);
oPersonQuery.MergeOption = MergeOption.NoTracking;
    Owner owner = oPersonQuery.FirstOrDefault();
    //dog.OwnerReference.IsLoaded == false;

Tracking ist sehr nützlich und in einer perfekten Welt ohne Leistungsprobleme immer aktiv. Aber in dieser Welt gibt es einen Preis dafür in Bezug auf die Leistung. Sollten Sie NoTracking verwenden, um die Dinge zu beschleunigen? Dies hängt davon ab, wofür Sie die Daten verwenden möchten.

Besteht die Möglichkeit, dass die Daten, die Sie mit NoTracking abfragen, zum Aktualisieren / Einfügen / Löschen in der Datenbank verwendet werden können? Verwenden Sie in diesem Fall NoTracking nicht, da Zuordnungen nicht nachverfolgt werden und Ausnahmen ausgelöst werden.

Auf einer Seite, auf der die Datenbank absolut nicht aktualisiert wird, können Sie NoTracking verwenden.

Das Mischen von Tracking und NoTracking ist möglich, erfordert jedoch besondere Vorsicht beim Aktualisieren / Einfügen / Löschen. Das Problem ist, dass beim Mischen das Risiko besteht, dass das Framework versucht, ein NoTracking-Objekt an den Kontext anzuhängen (), in dem eine andere Kopie desselben Objekts mit aktivierter Nachverfolgung vorhanden ist. Grundsätzlich sage ich das

Dog dog1 = (from dog in YourContext.DogSet where dog.ID == 2).FirstOrDefault();

ObjectQuery<Dog> oDogQuery = (ObjectQuery<Dog>)
    (from dog in YourContext.DogSet where dog.ID == 2 select dog);
oDogQuery.MergeOption = MergeOption.NoTracking;
Dog dog2 = oDogQuery.FirstOrDefault();

dog1 und dog2 sind 2 verschiedene Objekte, eines verfolgt und eines nicht. Wenn Sie das getrennte Objekt in einem Update / Insert verwenden, wird ein Attach () erzwungen, der besagt: "Moment mal, ich habe hier bereits ein Objekt mit demselben Datenbankschlüssel. Fail". Und wenn Sie ein Objekt anhängen (), wird auch die gesamte Hierarchie angehängt, was überall zu Problemen führt. Sei besonders vorsichtig.

Wie viel schneller ist es mit NoTracking

Das hängt von den Abfragen ab. Einige sind für die Verfolgung viel anfälliger als andere. Ich habe keine schnelle und einfache Regel dafür, aber es hilft.

Also sollte ich NoTracking dann überall verwenden?

Nicht genau. Das Verfolgen von Objekten bietet einige Vorteile. Das erste ist, dass das Objekt zwischengespeichert wird, sodass ein nachfolgender Aufruf für dieses Objekt die Datenbank nicht trifft. Dieser Cache ist nur für die Lebensdauer des YourEntities-Objekts gültig. Wenn Sie den obigen Singleton-Code verwenden, entspricht dies der Lebensdauer der Seite. Eine Seitenanforderung == ein YourEntity-Objekt. Bei mehreren Aufrufen für dasselbe Objekt wird es also nur einmal pro Seitenanforderung geladen. (Ein anderer Caching-Mechanismus könnte dies erweitern).

Was passiert, wenn Sie NoTracking verwenden und versuchen, dasselbe Objekt mehrmals zu laden? Die Datenbank wird jedes Mal abgefragt, daher gibt es dort Auswirkungen. Wie oft müssen Sie während einer einzelnen Seitenanforderung dasselbe Objekt aufrufen? Natürlich so wenig wie möglich, aber es passiert.

Erinnern Sie sich auch an das obige Stück über die automatische Verbindung der Assoziationen für Sie? Bei NoTracking ist dies nicht der Fall. Wenn Sie also Ihre Daten in mehreren Stapeln laden, haben Sie keine Verknüpfung zwischen diesen:

ObjectQuery<Dog> oDogQuery = (ObjectQuery<Dog>)(from dog in YourContext.DogSet select dog);
oDogQuery.MergeOption = MergeOption.NoTracking;
List<Dog> dogs = oDogQuery.ToList();

ObjectQuery<Person> oPersonQuery = (ObjectQuery<Person>)(from o in YourContext.PersonSet  select o);
oPersonQuery.MergeOption = MergeOption.NoTracking;
    List<Person> owners = oPersonQuery.ToList();

In diesem Fall wird für keinen Hund die Eigenschaft .Owner festgelegt.

Einige Dinge, die Sie beachten sollten, wenn Sie versuchen, die Leistung zu optimieren.

Kein faules Laden, was soll ich tun?

Dies kann als Segen in der Verkleidung angesehen werden. Natürlich ist es ärgerlich, alles manuell zu laden. Es verringert jedoch die Anzahl der Aufrufe der Datenbank und zwingt Sie, darüber nachzudenken, wann Sie Daten laden sollten. Je mehr Sie in einen Datenbankaufruf laden können, desto besser. Das war immer wahr, aber es wird jetzt mit dieser 'Funktion' von EF durchgesetzt.

Natürlich können Sie if (! ObjectReference.IsLoaded) ObjectReference.Load () aufrufen; Wenn Sie möchten, ist es jedoch besser, das Framework zu zwingen, die Objekte, von denen Sie wissen, dass Sie sie benötigen, auf einmal zu laden. Hier beginnt die Diskussion über parametrisierte Includes Sinn zu machen.

Nehmen wir an, Sie haben Ihr Hundeobjekt

public class Dog
{
    public Dog Get(int id)
    {
        return YourContext.DogSet.FirstOrDefault(it => it.ID == id );
    }
}

Dies ist die Art von Funktion, mit der Sie ständig arbeiten. Es wird von überall her aufgerufen und sobald Sie dieses Hundeobjekt haben, werden Sie in verschiedenen Funktionen sehr unterschiedliche Dinge damit tun. Erstens sollte es vorkompiliert werden, da Sie das sehr oft aufrufen werden. Zweitens möchten die verschiedenen Seiten Zugriff auf eine andere Teilmenge der Hundedaten haben. Einige wollen den Besitzer, andere das FavoriteToy usw.

Natürlich können Sie Load () für jede benötigte Referenz jederzeit aufrufen, wenn Sie eine benötigen. Dadurch wird jedoch jedes Mal ein Aufruf der Datenbank generiert. Schlechte Idee. Stattdessen fragt jede Seite nach den Daten, die sie sehen möchte, wenn sie das Dog-Objekt zum ersten Mal anfordert:

    static public Dog Get(int id) { return GetDog(entity,"");}
    static public Dog Get(int id, string includePath)
{
        string query = "select value o " +
            " from YourEntities.DogSet as o " +

15
Im Ernst, haben Sie keinen eigenen Blog oder eine eigene Website, auf der Sie diesen Aufsatz hätten veröffentlichen können?
AnthonyWJones

7
@AnthonyWJones, na ja, obwohl es nicht der übliche Beitrag auf SO ist; Wenn Sie sich den SO-Podcast anhören, verwendet Jeff immer das Mantra, dass SO ein Ort ist, an dem Programmierer Wissen austauschen können, das keine Blogs hat. Das ist NICHT anstößig. Und basierend auf Jeffs Mantra, sollte aus meiner Sicht nicht geschlossen werden.
BobbyShaftoe

2
Ich habe es als Community-Wiki gepostet, in der Hoffnung, dass es verbessert werden kann, was ein Blog nicht bieten kann. Ich bin ziemlich neu hier und höre mir auch den Podcast an, also fand ich das in Ordnung. Ich werde lernen.
ADB

4
@AD, kein Problem. Diese Gemeinschaft kann manchmal ziemlich lächerlich und faschistisch sein. :) Ich nehme an, Sie könnten einfach diesen Beitrag nehmen und den gesamten Text außer der Grundfrage löschen. Dann poste einfach eine Antwort. Magischerweise wird das wahrscheinlich positiv bewertet. Ich denke, wir mögen Bürokratie. :)
BobbyShaftoe

7
Ich bin nicht sonderlich besorgt über die Länge oder Komplexität dieser Frage, außer zu sagen, dass dies 31 Fragen sind, die in einer zusammengefasst sind. Wenn Sie sie so präsentieren, werden Sie wahrscheinlich keine nützlichen Antworten erhalten, was den Nutzen dieses Beitrags für uns alle verringert. Sie können 15-20 Beiträge erneut veröffentlichen und aufteilen.
Ben Collins

Antworten:


3

Bitte verwenden Sie nicht alle oben genannten Informationen wie "Singleton-Zugriff". Sie sollten diesen Kontext absolut nicht zu 100% speichern, um ihn wiederzuverwenden, da er nicht threadsicher ist.


+1 Es ist sicherlich gegen den Ansatz der Arbeitseinheit. Das Abbrechen von Änderungen wird nicht unterstützt, und wenn Sie den Kontext speichern, haben Sie Probleme zu wissen, was seit dem letzten Speichern tatsächlich geändert wurde.
Surfen

1

Obwohl informativ, denke ich, dass es hilfreicher sein kann, zu teilen, wie all dies in eine vollständige Lösungsarchitektur passt. Beispiel - Sie haben eine Lösung erhalten, die zeigt, wo Sie sowohl die EF-Vererbung als auch Ihre Alternative verwenden, damit der Leistungsunterschied angezeigt wird.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.