C # Entity-Framework: Wie kann ich ein .Find und .Include für ein Modellobjekt kombinieren?

145

Ich mache das mvcmusicstore-Übungs-Tutorial. Beim Erstellen des Gerüsts für den Album-Manager ist mir etwas aufgefallen (Hinzufügen, Löschen, Bearbeiten).

Ich möchte Code elegant schreiben, also suche ich nach einer sauberen Möglichkeit, dies zu schreiben.

Zu Ihrer Information, ich mache den Laden allgemeiner:

Alben = Artikel

Genres = Kategorien

Künstler = Marke

So wird der Index abgerufen (von MVC generiert):

var items = db.Items.Include(i => i.Category).Include(i => i.Brand);

So wird das zu löschende Element abgerufen:

Item item = db.Items.Find(id);

Der erste bringt alle Artikel zurück und füllt die Kategorie- und Markenmodelle innerhalb des Artikelmodells. Der zweite Teil füllt die Kategorie und die Marke nicht aus.

Wie kann ich den zweiten schreiben, um zu finden UND zu füllen, was sich darin befindet (vorzugsweise in einer Zeile) ... theoretisch - so etwas wie:

Item item = db.Items.Find(id).Include(i => i.Category).Include(i => i.Brand);
Ralph N.
quelle
Wenn jemand dies generisch in.net-core tun muss, siehe meine Antwort
Johnny 5

Antworten:

162

Sie müssen Include()zuerst ein einzelnes Objekt aus der resultierenden Abfrage verwenden und dann abrufen:

Item item = db.Items
              .Include(i => i.Category)
              .Include(i => i.Brand)
              .SingleOrDefault(x => x.ItemId == id);
Dennis Traub
quelle
24
Ich würde wirklich empfehlen, letzteres zu verwenden (SingleOrDefault). ToList ruft zuerst alle Einträge ab und wählt dann einen aus
Sander Rijken
5
Dies bricht zusammen, wenn wir einen zusammengesetzten Primärschlüssel haben und die entsprechende Suchüberladung verwenden.
Jhappoldt
78
Dies würde funktionieren, aber es gibt einen Unterschied zwischen der Verwendung von "Suchen" und "SingleOrDefault". Die "Find" -Methode gibt das Objekt aus dem lokalen nachverfolgten Speicher zurück, falls vorhanden, und vermeidet einen Roundtrip zur Datenbank. Bei Verwendung von "SingleOrDefault" wird eine Abfrage ohnehin zur Datenbank erzwungen.
Iravanchi
3
@Iravanchi ist richtig. Dies mag für den Benutzer funktioniert haben, aber der Vorgang und seine Nebenwirkungen sind meines Wissens nicht gleichbedeutend mit Suchen.
Mwilson
3
Beantwortet die ops-Frage nicht wirklich, da sie nicht verwendet wird. Finden
Paul
73

Dennis 'Antwort ist IncludeundSingleOrDefault . Letzteres geht in die Datenbank.

Eine Alternative ist die Verwendung Findin Kombination mitLoad zum expliziten Laden verwandter Entitäten ...

Unten ein MSDN-Beispiel :

using (var context = new BloggingContext()) 
{ 
  var post = context.Posts.Find(2); 

  // Load the blog related to a given post 
  context.Entry(post).Reference(p => p.Blog).Load(); 

  // Load the blog related to a given post using a string  
  context.Entry(post).Reference("Blog").Load(); 

  var blog = context.Blogs.Find(1); 

  // Load the posts related to a given blog 
  context.Entry(blog).Collection(p => p.Posts).Load(); 

  // Load the posts related to a given blog  
  // using a string to specify the relationship 
  context.Entry(blog).Collection("Posts").Load(); 
}

Gibt natürlich Findsofort zurück, ohne eine Anfrage an das Geschäft zu stellen, wenn diese Entität bereits vom Kontext geladen wurde.

Lerner
quelle
30
Bei dieser Methode wird Findfür die Entität selbst kein Roundtrip zur Datenbank durchgeführt , wenn die Entität vorhanden ist. ABER Sie haben eine Rundreise für jede Beziehung, die Sie Loadeingehen, während die SingleOrDefaultKombination mit Includealles auf einmal lädt.
Iravanchi
Beim Vergleich der 2 im SQL-Profiler war Find / Load für meinen Fall besser (ich hatte eine 1: 1-Beziehung). @Iravanchi: Wollen Sie damit sagen, wenn ich eine 1: m-Beziehung hätte, hätte es m mal den Laden genannt? ... weil es nicht so viel Sinn machen würde.
Lerner
3
Keine 1: m-Beziehung, sondern mehrere Beziehungen. Jedes Mal, wenn Sie die LoadFunktion aufrufen , sollte die Beziehung ausgefüllt werden, wenn der Aufruf zurückkehrt. Wenn Sie also Loadmehrmals für mehrere Beziehungen anrufen , wird jedes Mal eine Rundreise durchgeführt. Selbst für eine einzelne Beziehung führt die FindMethode zwei Roundtrips durch , wenn die Methode die Entität nicht im Speicher findet: einen für Findund einen für Load. Aber die Include. SingleOrDefaultAnsatz holt die Entität und Beziehung auf einmal, soweit ich weiß (aber ich bin nicht sicher)
Iravanchi
1
Es wäre schön gewesen, wenn das Design Include irgendwie hätte folgen können, anstatt Sammlungen und Referenzen anders behandeln zu müssen. Das macht es schwieriger, eine GetById () -Fassade zu erstellen, die nur eine optionale Sammlung von Expression <Func <T, Objekt >> enthält (z. B. _repo.GetById (id, x => x.MyCollection))
Derek Greer
4
Erwähnen Sie bitte
Hossein
1

Sie müssen IQueryable in DbSet umwandeln

var dbSet = (DbSet<Item>) db.Set<Item>().Include("");

return dbSet.Find(id);

Rafael R. Souza
quelle
Das dbSet enthält kein .Find oder .FindAsync. Ist das EF Core?
Thierry
es gibt ef 6 auch auf ef core
Rafael R. Souza
Ich war hoffnungsvoll und dann "InvalidCastException"
ZX9
0

Hat nicht für mich gearbeitet. Aber ich habe es so gelöst.

var item = db.Items
             .Include(i => i.Category)
             .Include(i => i.Brand)
             .Where(x => x.ItemId == id)
             .First();

Ich weiß nicht, ob das eine gute Lösung ist. Aber der andere, den Dennis gab, gab mir einen Fehler .SingleOrDefault(x => x.ItemId = id);

Johan
quelle
4
Dennis 'Lösung muss auch funktionieren. Vielleicht haben Sie diesen Fehler SingleOrDefault(x => x.ItemId = id)nur wegen des falschen Single =statt Double ==?
Slauma
6
Ja, sieht so aus, als hättest du = nicht == verwendet. Syntaxfehler;)
Ralph N
Ich habe beide ausprobiert == und = gab mir immer noch einen Fehler in .SingleOrDefault (x => x.ItemId = id); = / Muss etwas anderes in meinem Code sein, das falsch ist. Aber wie ich es gemacht habe, ist es schlecht? Vielleicht verstehe ich nicht, was du meinst. Dennis hat auch einen Singel = in seinem Code.
Johan
0

Es gibt keine wirklich einfache Möglichkeit, mit einem Fund zu filtern. Ich habe mir jedoch einen genauen Weg ausgedacht, um die Funktionalität zu replizieren. Beachten Sie jedoch einige Punkte für meine Lösung.

Mit dieser Lösung können Sie generisch filtern, ohne den Primärschlüssel in .net-core zu kennen

  1. Die Suche unterscheidet sich grundlegend, da die Entität abgerufen wird, wenn sie in der Nachverfolgung vorhanden ist, bevor die Datenbank abgefragt wird.

  2. Darüber hinaus kann es nach einem Objekt filtern, sodass der Benutzer den Primärschlüssel nicht kennen muss.

  3. Diese Lösung ist für EntityFramework Core.

  4. Dies erfordert Zugriff auf den Kontext

Hier sind einige Erweiterungsmethoden, die Sie hinzufügen können, um nach Primärschlüssel zu filtern

    public static IReadOnlyList<IProperty> GetPrimaryKeyProperties<T>(this DbContext dbContext)
    {
        return dbContext.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties;
    }

    //TODO Precompile expression so this doesn't happen everytime
    public static Expression<Func<T, bool>> FilterByPrimaryKeyPredicate<T>(this DbContext dbContext, object[] id)
    {
        var keyProperties = dbContext.GetPrimaryKeyProperties<T>();
        var parameter = Expression.Parameter(typeof(T), "e");
        var body = keyProperties
            // e => e.PK[i] == id[i]
            .Select((p, i) => Expression.Equal(
                Expression.Property(parameter, p.Name),
                Expression.Convert(
                    Expression.PropertyOrField(Expression.Constant(new { id = id[i] }), "id"),
                    p.ClrType)))
            .Aggregate(Expression.AndAlso);
        return Expression.Lambda<Func<T, bool>>(body, parameter);
    }

    public static Expression<Func<T, object[]>> GetPrimaryKeyExpression<T>(this DbContext context)
    {
        var keyProperties = context.GetPrimaryKeyProperties<T>();
        var parameter = Expression.Parameter(typeof(T), "e");
        var keyPropertyAccessExpression = keyProperties.Select((p, i) => Expression.Convert(Expression.Property(parameter, p.Name), typeof(object))).ToArray();
        var selectPrimaryKeyExpressionBody = Expression.NewArrayInit(typeof(object), keyPropertyAccessExpression);

        return Expression.Lambda<Func<T, object[]>>(selectPrimaryKeyExpressionBody, parameter);
    }

    public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this DbSet<TEntity> dbSet, DbContext context, object[] id)
        where TEntity : class
    {
        return FilterByPrimaryKey(dbSet.AsQueryable(), context, id);
    }

    public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this IQueryable<TEntity> queryable, DbContext context, object[] id)
        where TEntity : class
    {
        return queryable.Where(context.FilterByPrimaryKeyPredicate<TEntity>(id));
    }

Sobald Sie diese Erweiterungsmethoden haben, können Sie wie folgt filtern:

query.FilterByPrimaryKey(this._context, id);
Johnny 5
quelle