Entity Framework Abfragefähig asynchron

96

Ich arbeite mit Entity Framework 6 an einigen Web-API-Dingen. Eine meiner Controller-Methoden ist "Get All", bei der erwartet wird, dass der Inhalt einer Tabelle aus meiner Datenbank als empfangen wird IQueryable<Entity>. In meinem Repository frage ich mich, ob es einen vorteilhaften Grund gibt, dies asynchron zu tun, da ich neu in der Verwendung von EF mit Async bin.

Im Grunde läuft es darauf hinaus

 public async Task<IQueryable<URL>> GetAllUrlsAsync()
 {
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
 }

vs.

 public IQueryable<URL> GetAllUrls()
 {
    return context.Urls.AsQueryable();
 }

Wird die asynchrone Version hier tatsächlich Leistungsvorteile bringen, oder entsteht unnötiger Overhead, wenn ich zuerst auf eine Liste projiziere (wohlgemerkt asynchron) und dann zu IQueryable gehe?

Jesse Carter
quelle
1
context.Urls ist vom Typ DbSet <URL>, der IQueryable <URL> implementiert, sodass .AsQueryable () redundant ist. msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx Angenommen, Sie haben die von EF bereitgestellten Muster befolgt oder das Tool verwendet, das den Kontext für Sie erstellt.
Sean B

Antworten:

222

Das Problem scheint zu sein, dass Sie falsch verstanden haben, wie asynchron / warten Sie mit Entity Framework arbeiten.

Informationen zum Entity Framework

Schauen wir uns also diesen Code an:

public IQueryable<URL> GetAllUrls()
{
    return context.Urls.AsQueryable();
}

und Anwendungsbeispiel:

repo.GetAllUrls().Where(u => <condition>).Take(10).ToList()

Was passiert da?

  1. Wir erhalten ein IQueryableObjekt (das noch nicht auf die Datenbank zugreift) mitrepo.GetAllUrls()
  2. Wir erstellen ein neues IQueryableObjekt mit der angegebenen Bedingung mit.Where(u => <condition>
  3. Wir erstellen ein neues IQueryableObjekt mit dem angegebenen Paging-Limit mit.Take(10)
  4. Wir rufen Ergebnisse aus der Datenbank mit ab .ToList(). Unser IQueryableObjekt ist zu SQL (like select top 10 * from Urls where <condition>) kompiliert . Und die Datenbank kann Indizes verwenden. Der SQL Server sendet Ihnen nur 10 Objekte aus Ihrer Datenbank (nicht alle Milliarden in der Datenbank gespeicherten URLs).

Okay, schauen wir uns den ersten Code an:

public async Task<IQueryable<URL>> GetAllUrlsAsync()
{
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
}

Mit dem gleichen Anwendungsbeispiel haben wir:

  1. Wir laden alle Milliarden in Ihrer Datenbank gespeicherten URLs mit in den Speicher await context.Urls.ToListAsync();.
  2. Wir haben Speicherüberlauf. Richtiger Weg, um Ihren Server zu töten

Über async / warten

Warum wird Async / Warten bevorzugt verwendet? Schauen wir uns diesen Code an:

var stuff1 = repo.GetStuff1ForUser(userId);
var stuff2 = repo.GetStuff2ForUser(userId);
return View(new Model(stuff1, stuff2));

was geschieht hier?

  1. Beginnend in Zeile 1 var stuff1 = ...
  2. Wir senden eine Anfrage an den SQL Server, für den wir etwas Zeug1 bekommen möchten userId
  3. Wir warten (aktueller Thread ist blockiert)
  4. Wir warten (aktueller Thread ist blockiert)
  5. ..... .....
  6. SQL Server senden uns Antwort
  7. Wir gehen zu Zeile 2 über var stuff2 = ...
  8. Wir senden eine Anfrage an den SQL Server, für den wir etwas Zeug2 bekommen möchten userId
  9. Wir warten (aktueller Thread ist blockiert)
  10. Und wieder
  11. ..... .....
  12. SQL Server senden uns Antwort
  13. Wir rendern Ansicht

Schauen wir uns also eine asynchrone Version davon an:

var stuff1Task = repo.GetStuff1ForUserAsync(userId);
var stuff2Task = repo.GetStuff2ForUserAsync(userId);
await Task.WhenAll(stuff1Task, stuff2Task);
return View(new Model(stuff1Task.Result, stuff2Task.Result));

was geschieht hier?

  1. Wir senden eine Anfrage an den SQL Server, um stuff1 zu erhalten (Zeile 1)
  2. Wir senden eine Anfrage an den SQL Server, um stuff2 zu erhalten (Zeile 2)
  3. Wir warten auf Antworten vom SQL Server, aber der aktuelle Thread ist nicht blockiert. Er kann Anfragen von anderen Benutzern bearbeiten
  4. Wir rendern Ansicht

Richtiger Weg, es zu tun

Also guter Code hier:

using System.Data.Entity;

public IQueryable<URL> GetAllUrls()
{
   return context.Urls.AsQueryable();
}

public async Task<List<URL>> GetAllUrlsByUser(int userId) {
   return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();
}

Beachten Sie, dass Sie hinzufügen müssen, using System.Data.Entityum die Methode ToListAsync()für IQueryable zu verwenden.

Beachten Sie, dass Sie nicht arbeiten müssen, wenn Sie kein Filtern, Blättern und ähnliches benötigen IQueryable. Sie können nur await context.Urls.ToListAsync()materialisiert verwenden und damit arbeiten List<Url>.

Viktor Lova
quelle
3
@ Korijn Blick auf Bild i2.iis.net/media/7188126/… aus Einführung in die IIS-Architektur Ich kann sagen, dass alle Anforderungen in IIS asynchron verarbeitet werden
Viktor Lova
7
Da Sie nicht auf die Ergebnismenge in der GetAllUrlsByUserMethode reagieren, müssen Sie sie nicht asynchronisieren. Geben Sie einfach die Aufgabe zurück und sparen Sie sich eine unnötige Zustandsmaschine, die vom Compiler nicht generiert wird.
Johnathon Sullinger
1
@JohnathonSullinger Obwohl das in fröhlichem Fluss funktionieren würde, hat das nicht den Nebeneffekt, dass hier keine Ausnahme auftaucht und sich an den ersten Ort ausbreitet, der auf Sie wartet? (Nicht, dass das unbedingt schlecht wäre, aber es ist eine Verhaltensänderung?)
Henry war
9
Interessanterweise bemerkt niemand, dass das zweite Codebeispiel in "Über Async / Warten" völlig unsinnig ist, da es eine Ausnahme auslösen würde, da weder EF noch EF Core threadsicher sind. Wenn Sie also versuchen, parallel zu laufen, wird nur eine Ausnahme ausgelöst
Tseng
1
Obwohl diese Antwort richtig ist, würde ich empfehlen, die Verwendung zu vermeiden asyncund awaitwenn Sie nichts mit der Liste tun. Lassen Sie den Anrufer dazu await. Wenn Sie zu diesem Zeitpunkt auf den Aufruf warten, return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();erstellen Sie einen zusätzlichen asynchronen Wrapper, wenn Sie die Assembly dekompilieren und die IL anzeigen.
Ali Khakpouri
10

Es gibt einen massiven Unterschied in dem Beispiel, das Sie gepostet haben, der ersten Version:

var urls = await context.Urls.ToListAsync();

Dies ist schlecht , es gibt im Grunde genommen select * from tablealle Ergebnisse in den Speicher zurück und wendet dann das wheregegen das in der Speichersammlung an, anstatt select * from table where...gegen die Datenbank.

Die zweite Methode trifft die Datenbank erst dann, wenn eine Abfrage auf die angewendet wird IQueryable(wahrscheinlich über eine .Where().Select()Operation im Linq- Stil, die nur die DB-Werte zurückgibt , die mit der Abfrage übereinstimmen.

Wenn Ihre Beispiele vergleichbar asyncwären , wäre die Version normalerweise pro Anforderung etwas langsamer, da die Zustandsmaschine, die der Compiler generiert, um die asyncFunktionalität zu ermöglichen, mehr Overhead verursacht .

Der Hauptunterschied (und der Vorteil) besteht jedoch darin, dass die asyncVersion mehr gleichzeitige Anforderungen zulässt, da der Verarbeitungsthread nicht blockiert wird, während auf den Abschluss der E / A gewartet wird (Datenbankabfrage, Dateizugriff, Webanforderung usw.).

Trevor Pilley
quelle
7
bis eine Abfrage auf IQueryable angewendet wird .... weder IQueryable.Where noch IQueryable.Select erzwingen die Ausführung der Abfrage. Der Prior wendet ein Prädikat an und der letztere eine Projektion. Es wird erst ausgeführt, wenn ein Materialisierungsoperator wie ToList, ToArray, Single oder First verwendet wird.
JJS
0

Kurz gesagt,
IQueryabledient dazu, den RUN-Prozess zu verschieben und den Ausdruck zunächst in Verbindung mit anderen IQueryableAusdrücken zu erstellen. Anschließend wird der Ausdruck als Ganzes interpretiert und ausgeführt.
Aber ToList()Methode (oder ein paar solche Methoden) sollen den Ausdruck sofort "so wie er ist" ausführen.
Ihre erste Methode ( GetAllUrlsAsync) wird sofort ausgeführt, da darauf IQueryabledie ToListAsync()Methode folgt . Daher wird es sofort ausgeführt (asynchron) und gibt eine Reihe von IEnumerables zurück.
In der Zwischenzeit wird Ihre zweite Methode ( GetAllUrls) nicht ausgeführt. Stattdessen wird ein Ausdruck zurückgegeben, und CALLER dieser Methode ist für die Ausführung des Ausdrucks verantwortlich.

Rzassar
quelle