Entity Framework interrogeable async

97

Je travaille sur quelques trucs d'API Web en utilisant Entity Framework 6 et l'une de mes méthodes de contrôleur est un "Get All" qui s'attend à recevoir le contenu d'une table de ma base de données en tant que IQueryable<Entity>. Dans mon référentiel, je me demande s'il existe une raison avantageuse de le faire de manière asynchrone, car je suis nouveau dans l'utilisation d'EF avec async.

Fondamentalement, cela se résume à

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

contre

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

La version asynchrone apportera-t-elle réellement des avantages en termes de performances ici ou est-ce que je subis une surcharge inutile en projetant d'abord sur une liste (en utilisant async, vous l'esprit) et ALORS en allant à IQueryable?

Jesse Carter
la source
1
context.Urls est de type DbSet <URL> qui implémente IQueryable <URL> donc le .AsQueryable () est redondant. msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx En supposant que vous ayez suivi les modèles fournis par EF ou utilisé les outils qui créent le contexte pour vous.
Sean B

Réponses:

223

Le problème semble être que vous avez mal compris comment async / await fonctionne avec Entity Framework.

À propos d'Entity Framework

Alors, regardons ce code:

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

et exemple d'utilisation:

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

Que se passe-t-il là?

  1. Nous obtenons un IQueryableobjet (pas encore accès à la base de données) en utilisantrepo.GetAllUrls()
  2. Nous créons un nouvel IQueryableobjet avec une condition spécifiée en utilisant.Where(u => <condition>
  3. Nous créons un nouvel IQueryableobjet avec une limite de pagination spécifiée en utilisant.Take(10)
  4. Nous récupérons les résultats de la base de données en utilisant .ToList(). Notre IQueryableobjet est compilé en sql (like select top 10 * from Urls where <condition>). Et la base de données peut utiliser des index, le serveur sql ne vous envoie que 10 objets de votre base de données (pas tous les milliards d'urls stockés dans la base de données)

Bon, regardons le premier code:

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

Avec le même exemple d'utilisation, nous avons obtenu:

  1. Nous chargeons en mémoire tous les milliards d'urls stockés dans votre base de données en utilisant await context.Urls.ToListAsync();.
  2. Nous avons un débordement de mémoire. Bonne façon de tuer votre serveur

À propos de async / await

Pourquoi async / await est-il préférable d'utiliser? Regardons ce code:

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

Que se passe t-il ici?

  1. À partir de la ligne 1 var stuff1 = ...
  2. Nous envoyons une requête au serveur SQL pour laquelle nous voulons obtenir des informations userId
  3. On attend (le thread actuel est bloqué)
  4. On attend (le thread actuel est bloqué)
  5. .....
  6. Le serveur SQL nous envoie une réponse
  7. Nous passons à la ligne 2 var stuff2 = ...
  8. Nous envoyons une requête au serveur SQL pour laquelle nous voulons obtenir des trucs2 userId
  9. On attend (le thread actuel est bloqué)
  10. Et encore
  11. .....
  12. Le serveur SQL nous envoie une réponse
  13. Nous rendons la vue

Regardons donc une version asynchrone de celui-ci:

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

Que se passe t-il ici?

  1. Nous envoyons une demande au serveur SQL pour obtenir stuff1 (ligne 1)
  2. Nous envoyons une demande au serveur SQL pour obtenir stuff2 (ligne 2)
  3. Nous attendons les réponses du serveur sql, mais le thread actuel n'est pas bloqué, il peut gérer les requêtes d'un autre utilisateur
  4. Nous rendons la vue

Bonne façon de le faire

Tellement bon code ici:

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

Notez que vous devez ajouter using System.Data.Entitypour utiliser la méthode ToListAsync()pour IQueryable.

Notez que si vous n'avez pas besoin de filtrage, de pagination et d'autres choses, vous n'avez pas besoin de travailler avec IQueryable. Vous pouvez simplement utiliser await context.Urls.ToListAsync()et travailler avec matérialisé List<Url>.

Viktor Lova
la source
3
@Korijn regardant l'image i2.iis.net/media/7188126/… de Introduction à l'architecture IIS Je peux dire que toutes les demandes dans IIS sont traitées de manière asynchrone
Viktor Lova
7
Puisque vous n'agissez pas sur le jeu de résultats dans la GetAllUrlsByUserméthode, vous n'avez pas besoin de le rendre asynchrone. Renvoyez simplement la tâche et évitez que la machine à états inutile ne soit générée par le compilateur.
Johnathon Sullinger
1
@JohnathonSullinger Bien que cela fonctionnerait dans un flux heureux, cela n'a-t-il pas pour effet secondaire qu'aucune exception ne fera surface ici et ne se propagera au premier endroit qui a une attente? (Pas que ce soit forcément mauvais, mais c'est un changement de comportement?)
Henry Been
9
Intéressant personne ne remarque que le 2ème exemple de code dans "About async / await" est totalement insensé, car il lèverait une exception puisque ni EF ni EF Core ne sont thread-safe, donc essayer de s'exécuter en parallèle lèvera simplement une exception
Tseng
1
Bien que cette réponse soit correcte, je vous recommande d'éviter d'utiliser asyncet awaitsi vous ne faites rien avec la liste. Laissez l'appelant await. Lorsque vous attendez l'appel à ce stade, return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();vous créez un wrapper asynchrone supplémentaire lorsque vous décompilez l'assembly et examinez l'IL.
Ali Khakpouri
10

Il y a une énorme différence dans l'exemple que vous avez posté, la première version:

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

Ceci est mauvais , il le fait essentiellement select * from table, renvoie tous les résultats en mémoire, puis applique le wherecontre cela dans la collection de mémoire plutôt que de le faire select * from table where...contre la base de données.

La deuxième méthode n'atteindra pas réellement la base de données tant qu'une requête ne sera pas appliquée au IQueryable(probablement via une .Where().Select()opération de style linq qui ne retournera que les valeurs de base de données correspondant à la requête.

Si vos exemples étaient comparables, la asyncversion sera généralement légèrement plus lente par requête car il y a plus de surcharge dans la machine à états que le compilateur génère pour autoriser la asyncfonctionnalité.

Cependant, la principale différence (et avantage) est que la asyncversion autorise plus de requêtes simultanées car elle ne bloque pas le thread de traitement pendant qu'elle attend la fin des E / S (requête de base de données, accès au fichier, requête Web, etc.).

Trevor Pilley
la source
7
jusqu'à ce qu'une requête soit appliquée à IQueryable .... ni IQueryable.Where et IQueryable.Select ne forcent l'exécution de la requête. Le prieur applique un prédicat et ce dernier applique une projection. Il n'est pas exécuté tant qu'un opérateur de matérialisation n'est pas utilisé, comme ToList, ToArray, Single ou First.
JJS
0

En bref,
IQueryableest conçu pour reporter le processus RUN et d'abord construire l'expression en conjonction avec d'autres IQueryableexpressions, puis interpréter et exécuter l'expression dans son ensemble.
Mais la ToList()méthode (ou quelques sortes de méthodes comme celle-là), consiste à exécuter l'expression instantanément "telle quelle".
Votre première méthode ( GetAllUrlsAsync) s'exécutera immédiatement, car elle est IQueryablesuivie de ToListAsync()method. par conséquent, il s'exécute instantanément (asynchrone) et renvoie un tas de IEnumerables.
En attendant, votre deuxième méthode ( GetAllUrls) ne sera pas exécutée. Au lieu de cela, il renvoie une expression et CALLER de cette méthode est chargé d'exécuter l'expression.

Rzassar
la source