Was sind gute Entwurfspraktiken bei der Arbeit mit Entity Framework?

asp.net entity-framework visual-studio-2008

Frage

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

Dies ist ein Community-Beitrag, bitte fügen Sie ihn hinzu, wie Sie es für richtig halten.

Betrifft : Entity Framework 1.0, das mit Visual Studio 2008 SP1 ausgeliefert wird.

Warum sollte man EF überhaupt auswählen?

In Anbetracht 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 Untermenge von EF). Darüber hinaus sind Sie möglicherweise nicht mit NHibernate oder anderen Lösungen auf dem Markt zufrieden. Was auch immer die Gründe sind, es gibt Leute (einschließlich mir), die mit EF arbeiten, und das Leben ist nicht schlecht.

EF und Vererbung

Das erste große Thema ist das Erbe. EF unterstützt das Mapping für geerbte Klassen, die auf zwei Arten persistent sind: 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 von sind Ein Vererbungsbaum: Die generierte SQL ist unglaublich schrecklich, es dauert lange, bis sie von der EF analysiert wird, und es dauert auch lange, bis sie ausgeführt wird. Dies ist ein echter Showstopper. Genug, EF sollte wahrscheinlich nicht mit Vererbung oder so wenig wie möglich verwendet werden.

Hier ist ein Beispiel, wie schlecht es war. Mein EF-Modell hatte ~ 30 Klassen, von denen ~ 10 Teil eines Vererbungsbaums waren. Bei der Ausführung einer Abfrage, um ein Element aus der Base-Klasse abzurufen, so einfach wie Base.Get (id), bestand die generierte SQL aus mehr als 50.000 Zeichen. Wenn Sie dann versuchen, einige Assoziationen zurückzugeben, degeneriert dies sogar noch mehr und geht so weit, dass SQL-Ausnahmen ausgelöst werden, da nicht mehr als 256 Tabellen gleichzeitig abgefragt werden können.

Ok, das ist schlecht. EF-Konzept ermöglicht es Ihnen, Ihre Objektstruktur ohne (oder mit so wenig wie möglich) der tatsächlichen Datenbankimplementierung Ihrer Tabelle zu erstellen. Daran scheitert es völlig.

Also Empfehlungen? Vermeiden Sie Vererbung, wenn Sie können, die Leistung wird so viel besser sein. Verwenden Sie es sparsam dort, wo Sie müssen. Meiner Meinung nach macht dies EF zu einem verklärten SQL-Generierungswerkzeug für Abfragen, aber es hat immer noch Vorteile, es zu verwenden. Und Möglichkeiten, einen Mechanismus zu implementieren, der der Vererbung ähnelt.

Vererbung mit Schnittstellen umgehen

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

Sie können Schnittstellen verwenden, um zu erzwingen, dass Klassen einige Funktionen implementieren. Hier ist zum Beispiel eine IEntity-Schnittstelle, mit der Sie Assoziationen zwischen EF-Entitäten definieren können, von denen Sie zur Entwurfszeit nicht wissen, welcher Entitätstyp es wäre.

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 diesem IEntity können Sie dann mit undefinierten Zuordnungen 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);
    }
}

was 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 den PetType in einem zusätzlichen Datenbankfeld speichern muss, aber angesichts der Leistungssteigerungen würde ich nicht zurückschauen.

Es kann auch keine Eins-zu-Viele-Viele-zu-Viele-Beziehung modellieren, aber mit kreativen Verwendungen von "Union" könnte es funktionieren. Schließlich erstellt es den Nebeneffekt des Ladens von Daten in eine Eigenschaft / Funktion des Objekts, auf die Sie achten 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 ist ähnlich wie bei Linq2Sql.

Was ist eine kompilierte Abfrage? Es handelt sich lediglich um eine Abfrage, für die Sie dem Framework sagen, dass die geparste Struktur im Speicher verbleiben soll, sodass sie bei der nächsten Ausführung nicht erneut erstellt werden muss. Beim nächsten Durchlauf sparen Sie sich die Zeit, die für die Analyse des Baums erforderlich ist. Rechnen Sie das nicht ab, da dies ein sehr kostspieliger Vorgang ist, der 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 bei Verwendung einer EntityDataSource auf Ihrer Seite tatsächlich ObjectQuery mit EntitySQL verwenden, sodass kompiliert und zwischengespeichert wird.)

Nebenbei, falls Sie nicht wissen, was EntitySQL ist. Es ist eine String-basierte Methode zum Schreiben von Abfragen gegen die EF. Hier ein Beispiel: "Wählen Sie den Wert dog aus Entities.DogSet als dog wobei dog.ID = @ID" aus. 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 also, wie Sie dies mit ObjectQuery <> tun

        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 die Ausdrucksbaumstruktur und speichert sie im Speicher. Wenn Sie das nächste Mal ausgeführt werden, 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 zur späteren Verwendung zu kompilieren, ist die CompiledQuery.Compile-Methode. Dies verwendet einen Delegierten:

    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 Kompilierzeit geprüft wird, während dies bei EntitySQL nicht der Fall ist. Es gibt jedoch andere Überlegungen ...

Enthält

Angenommen, 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 tun, richtig?

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());

Nun, was ist, wenn Sie das Include parametrisieren möchten? Was ich damit meine, ist, dass Sie eine einzige Get () - Funktion haben möchten, die von verschiedenen Seiten aufgerufen wird, die sich um unterschiedliche Beziehungen für den Hund kümmern. Man kümmert sich um den Besitzer, ein anderes um sein FavoriteFood, ein anderes um sein FavotireToy und so weiter. Grundsätzlich möchten Sie der Abfrage mitteilen, welche Verknüpfungen 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 Sie die Include (string) -Funktion (die nur einen einzigen Pfad akzeptiert) mit einem IncludeMany (string) verbessern kann, mit dem Sie eine Zeichenfolge von durch Kommas getrennten Zuordnungen zum Laden übergeben können. Suchen Sie im Erweiterungsabschnitt nach dieser Funktion.

Wenn wir versuchen, dies mit CompiledQuery zu tun, 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 verschlucken, wenn aufgerufen wird mit:

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

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

Dann verwenden wir 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());

Wieder falsch, diesmal weil die EF IncludeMany nicht parsen kann, weil sie nicht Teil der Funktionen ist, die erkannt werden: Es ist eine Erweiterung.

Ok, Sie möchten also eine beliebige Anzahl von Pfaden an Ihre Funktion übergeben und Includes () nimmt nur einen einzigen. Was ist zu tun? Sie könnten entscheiden, dass Sie niemals mehr benötigen werden, beispielsweise 20 Includes, und die einzelnen Zeichenfolgen 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 mal. Können wir mit CompiledQuery keine ObjectQuery <> zurückgeben? Dann die Includes auf das setzen? Nun, das hätte ich mir 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 Aufruf von IncludeMany (oder Include, Where, OrderBy ...) die zwischengespeicherte kompilierte Abfrage für ungültig erklären, da sie jetzt eine völlig neue ist! Daher muss der Ausdrucksbaum erneut analysiert werden, und Sie erhalten diese Leistung erneut.

Was ist also die Lösung? Sie können CompiledQueries einfach nicht mit parametrisierten Include-Dateien verwenden. Verwenden Sie stattdessen EntitySQL. Dies bedeutet nicht, dass CompiledQueries nicht verwendet wird. Dies ist ideal für lokalisierte Abfragen, die immer im selben Kontext aufgerufen werden. Idealerweise sollte CompiledQuery immer verwendet werden, da die Syntax zur Kompilierzeit geprüft wird. Dies ist jedoch aufgrund von Einschränkungen nicht möglich.

Ein Anwendungsbeispiel wäre: Sie möchten vielleicht eine Seite haben, auf der abgefragt wird, welche beiden Hunde dasselbe Lieblingsfutter haben, was für eine BusinessLayer-Funktion etwas eng ist, so dass Sie sie in Ihre Seite einfügen und genau wissen, welche Art von Includes es gibt erforderlich.

Übergeben 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. So haben Sie 3 Parameter. Eine Pech, kann aber sehr leicht verbessert werden.

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 die Ausführung der Abfrage normalerweise nicht bis zum letzten Moment, falls 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? Durch das Spielen mit der ursprünglichen ObjectQuery (das ist der tatsächliche Rückgabetyp der Linq-Anweisung, die IEnumerable implementiert), wird die kompilierte Abfrage ungültig, und es wird eine erneute Analyse erzwungen. Die Faustregel lautet 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. Später wird der OrderBy für die Objekte im Speicher ausgeführt. Es ist vielleicht etwas langsamer, aber ich bin mir nicht einmal sicher. Sicher ist jedoch, dass Sie sich keine Sorgen machen müssen, wenn Sie ObjectQuery falsch handhaben und den kompilierten Abfrageplan ungültig machen.

Auch dies ist keine pauschale Aussage. ToList () ist ein defensiver Programmier-Trick, aber wenn Sie einen gültigen Grund haben, ToList () nicht zu verwenden, machen Sie weiter. Es gibt viele Fälle, in denen Sie die Abfrage vor der Ausführung verfeinern möchten.

Performance

Welchen Einfluss hat die Leistung auf das Kompilieren einer Abfrage? Es kann tatsächlich ziemlich groß sein. Eine Faustregel besagt, dass das Kompilieren und Zwischenspeichern der Abfrage für die Wiederverwendung die doppelte Zeit dauert, sie einfach ohne Zwischenspeicherung auszuführen. Bei komplexen Abfragen (Lese-Erbeben) habe ich bis zu 10 Sekunden nach oben gesehen.

Wenn Sie zum ersten Mal eine vorkompilierte Abfrage aufrufen, erhalten Sie einen Performance-Erfolg. Nach diesem ersten Treffer ist die Leistung merklich besser als dieselbe nicht vorkompilierte Abfrage. Praktisch das gleiche wie Linq2Sql

Wenn Sie beim ersten Mal eine Seite mit vorkompilierten Abfragen laden, erhalten Sie einen Treffer. Es wird in etwa 5-15 Sekunden geladen (offensichtlich werden am Ende mehr als eine vorkompilierte Abfrage aufgerufen), während nachfolgende Ladevorgänge weniger als 300 ms dauern. Ein dramatischer Unterschied, und es liegt an Ihnen, zu entscheiden, ob es für Ihren ersten Benutzer in Ordnung ist, einen Treffer zu machen, oder Sie möchten, dass ein Skript Ihre Seiten aufruft, 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 Sie müssen jedes Mal, wenn Sie sie aufrufen, den Baum erzeugen.

Parametrisierte Abfragen

Die meisten Suchfunktionen beinhalten stark parametrisierte Abfragen. Es gibt sogar Bibliotheken, mit denen Sie eine parametrisierte Abfrage aus Lamba-Ausdrücken erstellen können. Das Problem ist, dass Sie keine vorkompilierten Abfragen damit verwenden können. Eine Möglichkeit, dies zu erreichen, besteht darin, alle möglichen Kriterien in der Abfrage abzubilden und zu kennzeichnen, welche 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 hier ist, dass Sie alle Vorteile eines vorkompilierten Quert erhalten. Die Nachteile sind, dass Sie höchstwahrscheinlich mit einer Where-Klausel enden werden, die ziemlich schwierig zu verwalten ist, dass Sie eine höhere Strafe für das Vorkompilieren der Abfrage erleiden und dass jede Abfrage, die Sie ausführen, nicht so effizient ist, wie es sein könnte (insbesondere mit eingeworfenen Verbindungen).

Eine andere Möglichkeit besteht darin, eine EntitySQL-Abfrage Stück für Stück zu erstellen, wie wir es alle mit SQL gemacht 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: - Es gibt keine Syntaxprüfung während des Kompilierens. - Jede andere Kombination von Parametern generiert eine andere Abfrage, die bei der ersten Ausführung vorkompiliert werden muss. In diesem Fall gibt es nur vier 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 mag es, Strings zu verketten!

Eine andere Option ist, eine große Teilmenge der Daten abzufragen und sie dann im Arbeitsspeicher 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 eine Menge gibt, aber Sie wissen auch, dass es nicht so viele gibt. Auf Ihrer CityDog-Suchseite können Sie alle Hunde für die Stadt in den Speicher laden. Hierbei handelt es sich um eine einzige vorkompilierte Abfrage, und verfeinern Sie 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 alle Daten anzeigen und dann filtern möchten.

Probleme: - Kann zu ernsthafter Datenübertragung führen, wenn Sie sich bei Ihrem Subset nicht umsehen. - Sie können nur nach den von Ihnen zurückgegebenen Daten filtern. Dies bedeutet, dass Sie den Dog.Owner.Name nicht filtern können, wenn Sie die Dog.Owner-Association nicht zurückgeben. 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 Erstellung von Lambda-basierten Abfragen, wenn Sie nicht daran interessiert sind, Ihre Abfragen vorab zu kompilieren. - Verwenden Sie eine vollständig vordefinierte 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 unterschiedlicher resultierender Abfragen klein ist (was weniger Treffer vor der Kompilierung bedeutet). - Verwenden Sie eine speicherinterne Filterung, wenn Sie mit einer kleinen Untermenge der Daten arbeiten oder wenn Sie zunächst alle Daten aus den Daten abrufen mussten (wenn die Leistung mit allen Daten in Ordnung ist, wird das Filtern im Speicher nicht durchgeführt.) jede Zeit in der Datenbank verbringen).

Singleton Zugang

Der beste Weg, um mit Ihrem Kontext und Ihren Entitäten auf allen Ihren Seiten umzugehen, ist das Singleton-Muster:

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, ist es das wert?

Wenn Sie eine Abfrage ausführen, können Sie dem Framework mitteilen, welche Objekte zurückgegeben werden sollen oder nicht. Was heißt das? Wenn die Nachverfolgung aktiviert ist (die Standardoption), verfolgt das Framework, was mit dem Objekt vor sich geht (wurde geändert? Erstellt? Gelöscht?). Außerdem werden Objekte miteinander verknüpft, wenn weitere Abfragen aus der Datenbank erfolgen ist hier von Interesse.

Nehmen wir zum Beispiel an, dass Dog 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 Tracking 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 Performanceproblem wäre es immer eingeschaltet. Aber in dieser Welt gibt es einen Preis für die Leistung. Also sollten Sie NoTracking verwenden, um die Dinge zu beschleunigen? Es hängt davon ab, wofür Sie die Daten verwenden möchten.

Gibt es eine Möglichkeit, dass die Daten, die Ihre Abfrage mit NoTracking abfragt, zum Aktualisieren / Einfügen / Löschen in der Datenbank verwendet werden können? Verwenden Sie in diesem Fall NoTracking nicht, da Assoziationen nicht nachverfolgt werden und Ausnahmen ausgelöst werden.

Auf einer Seite, auf der absolut keine Updates für die Datenbank vorhanden sind, können Sie NoTracking verwenden.

Das Mischen von Tracking und NoTracking ist möglich, aber Sie müssen beim Aktualisieren / Einfügen / Löschen besonders vorsichtig sein. Das Problem ist, dass Sie beim Mischen riskieren, dass das Framework versucht, ein NoTracking-Objekt an den Kontext anzuhängen, in dem sich eine weitere Kopie desselben Objekts mit Tracking befindet. 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. Die Verwendung des freistehenden Objekts in einer Aktualisierung / Einfügung erzwingt ein Attach () mit der Meldung "Warten Sie eine Minute, ich habe hier bereits ein Objekt mit demselben Datenbankschlüssel. Fehler". Wenn Sie ein Objekt anhängen (), wird auch seine gesamte Hierarchie angehängt, was überall Probleme verursacht. Seien Sie besonders vorsichtig.

Wie viel schneller geht es mit NoTracking

Das hängt von den Fragen ab. Einige sind viel leichter zu verfolgen als andere. Ich habe keine schnelle Regel dafür, aber es hilft.

Also sollte ich NoTracking dann überall verwenden?

Nicht genau. Die Objektverfolgung bietet einige Vorteile. Der erste ist, dass das Objekt zwischengespeichert wird, sodass der nachfolgende Aufruf dieses Objekts die Datenbank nicht berührt. Dieser Cache gilt nur für die Lebensdauer des YourEntities-Objekts. Wenn Sie den oben genannten Singleton-Code verwenden, entspricht er dem Seitenlebensdauer. Eine Seitenanfrage == ein YourEntity-Objekt. Bei mehreren Aufrufen für dasselbe Objekt wird es daher nur einmal pro Seitenanforderung geladen. (Andere Zwischenspeicherungsmechanismen könnten das verlängern).

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 rufen / rufen Sie dasselbe Objekt während einer einzelnen Seitenanfrage auf? Natürlich so wenig wie möglich, aber es passiert.

Erinnern Sie sich auch an den obigen Artikel darüber, dass die Assoziationen automatisch für Sie verbunden werden? Mit NoTracking ist das nicht möglich. Wenn Sie also Ihre Daten in mehreren Batches laden, haben Sie keine Verbindung zwischen ihnen:

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 hat kein Hund seine .Owner-Eigenschaft.

Einige Dinge, die Sie beachten sollten, wenn Sie die Leistung optimieren möchten.

Keine faule Ladung, was soll ich tun?

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

Natürlich können Sie if (! ObjectReference.IsLoaded) ObjectReference.Load () aufrufen. Wenn Sie möchten, ist es eine bessere Praxis, den Rahmen zu zwingen, die Objekte zu laden, von denen Sie wissen, dass Sie sie in einem Schuss benötigen. Hier macht die Diskussion über parametrisierte Includes Sinn.

Lass uns sagen, du hast ein Dog-Objekt

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

Mit dieser Art von Funktion arbeiten Sie ständig. Es wird von überall her aufgerufen und sobald Sie dieses Dog-Objekt haben, werden Sie in verschiedenen Funktionen sehr unterschiedliche Dinge 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 Dog-Daten haben. Einige wollen den Besitzer, einige das FavoriteToy usw.

Natürlich können Sie Load () für jede Referenz aufrufen, die Sie benötigen, wann immer Sie eine benötigen. Dadurch wird jedoch jedes Mal ein Aufruf an die 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 " +

Beliebte Antwort

Bitte verwenden Sie nicht alle oben genannten Informationen wie "Singleton-Zugriff". Sie sollten diesen Kontext zu 100% nicht für die Wiederverwendung speichern, da er nicht threadsicher ist.



Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum