-1
votes

Pourquoi ne pas mélanger les requêtes IQueryable et IEnumerable?

lors d'une requête dans linq, il est recommandé de ne pas combiner les parties iqeuryable et ienumerable . est-ce juste une question de performance ou quoi?

Je veux apporter quelques éclaircissements. un certain temps, il n'est pas possible de le faire (comme ce que @Harald Coppoolse a expliqué dans sa réponse. mais un certain temps c'est faisable mais je ne suis pas sûr de ce qui se passe avec la performance par exemple.

'from e in someIEnumerable
join q in someIQueryable on e.reference equals q.ID'

alors qu'arrivera-t-il à cette requête?


3 commentaires

Recommandé par qui, dans quelle situation? Il y a probablement un scénario où il est facile de montrer que c'est une mauvaise idée, mais ce n'est en aucun cas une vérité universelle. En cas de problème, cela vient généralement du passage involontaire d'un fournisseur LINQ (une base de données, un service ou autre) à LINQ to Objects, récupérant beaucoup plus de données du fournisseur que nécessaire car le fournisseur aurait pu les filtrer / les agréger lui-même. D'un autre côté, selon le fournisseur, c'est parfois le seul moyen de faire avancer les choses.


il semble que ce soit un problème lors de l'exécution de la partie iqueryable chaque fois qu'elle participe à l'ensemble de la requête. par exemple lorsqu'une partie iqueryable se joint à une partie ienumerable. Je pense que c'est correct de les mélanger dans la partie `` où '' lorsque vous voulez vérifier le confinement par exemple


La différence n'est pas tant entre IEnumerable et IQueryable , mais sur l'arborescence d'expression finale et quel fournisseur fait quelle partie de la requête. N'oubliez pas que les interfaces ne sont que des interfaces ; ils ne dictent pas le comportement. Il est parfaitement possible, par exemple, pour un fournisseur LINQ de prendre un IEnumerable , de voir qu'il s'agit en fait d'une simple collection et de le traduire en une requête efficace. (De nombreux fournisseurs de bases de données le font pour les requêtes de style IN .)


3 Réponses :


0
votes

Dans le contexte de LinQ to Entities

IQueryable étend IEnumerable et n'est qu'une expression de requête. Cela signifie qu'il ne résout pas les données dans le magasin de données Entity avant un .ToList() ou un FirstOrDefault() par exemple.

IEnumerable fournit la méthode GetEnumerator() , permettant de parcourir les collections déjà résolues .

Vous devez donc utiliser IQueryable jusqu'à votre couche d'accès aux données, puis utiliser IEnumerable lorsque vous obtenez déjà vos données.

Utiliser la bonne interface est un moyen de ne pas obtenir toutes les données inutiles et de travailler (par exemple, itérer) uniquement sur les données sur lesquelles vous devez travailler.

MODIFIÉ basé sur la réponse de JonSkeet


4 commentaires

"permettant d'itérer jeter la collection déjà résolue " - non, LINQ to Objects est toujours paresseux aussi.


@JonSkeet Je n'étais pas au courant de cela, je mettrai à jour ma réponse en conséquence.


Vous voudrez peut-être également remplacer «jeter» par «à travers»


@Skrface Juste comme un exemple rapide d'une collection non résolue utilisant IEnumerable , IEnumerable ce violon. Fiddle limite la sortie, mais si vous l'exécutiez localement, il semblerait que la collection contienne une séquence infinie de nombres aléatoires.



0
votes

Pour décider de continuer une instruction IQueryable LINQ avec un IEnumerable ou non, il est important de comprendre la différence entre les deux.

Un objet d'une classe qui implémente IEnumerable<...> est un objet qui représente une séquence. Il contient tout ce qu'il contient pour obtenir le premier élément de la séquence, et une fois que vous avez un élément, vous pouvez demander le suivant, s'il y a un élément suivant.

Cette énumération est lancée explicitement si vous utilisez IEnumerable.GetEnumerator() et IEnumerator.MoveNext() , ou implicitement lorsque vous utilisez des fonctions comme foreach , ToList() , ToDictionary() , FirstOrDefault() , Sum() , Count() , Any() , etc. Si vous étudiez le code source d'Enumerable , vous verrez qu'au fond, ils appellent GetEnumator() et MoveNext()

Bien qu'un objet d'une classe qui implémente IQueryable<...> représente également une séquence, il n'a pas besoin de savoir comment énumérer cette séquence. Il contient une Expression et un Provider . L' Expression est une forme très générique des données interrogées, le Provider sait qui doit exécuter la requête (assez souvent un système de gestion de base de données) et le langage utilisé pour communiquer avec cet exécuteur (généralement quelque chose de semblable à SQL).

La IQueryable objets IQueryable avec des méthodes qui retournent également IQueryable ne changera que l' Expression ; la base de données n'est pas contactée.

Lorsque vous appelez Queryable.GetEnumerator() pour commencer à énumérer la requête, l' Expression est envoyée au Provider qui traduira cette Expression en SQL et interrogera l'exécuteur. Les données renvoyées sont matérialisées dans une séquence IEnumerable. GetEnumerator() est appelé et l'énumérateur retourné est retourné, vous pouvez donc appeler MoveNext() et Current comme si vous MoveNext() un IEnumerable .

Étant donné que l' Expression doit être traduite en SQL, vous ne pouvez pas tout faire avec un IQueryable que vous pouvez faire avec un IEnumerable .

Ce qui suit fonctionnera avec un IEnumerable :

IQueryable<decimal> CalculateValueAddedTaxes(this IQueryable<Price> prices) {...}

Order newOrder = new Order() {...};      // A local object
newOrder.TotalVAT = newOrder.OrderLines  // A local sequence of OrderLInes
     .AsQueryable()                      // still Local, but now as IQueryable
     .CalculateValueAddedTaxex()         // so you can call this function
     .Sum();

Le compilateur ne peut détecter aucun problème avec les éléments suivants:

var result = orderLines
    .Where(orderLine => orderLine.HasValueAddedTax)
    .Select(orderLine => orderLine.Price)
    .OrderByDescending(price => price.Value)
    .CalculateValueAddedTaxes()
    .FirstOrDefault();

Bien que cela se compile, vous obtiendrez une exception d'exécution, vous indiquant que CalculateValueAddedTax ne peut pas être traduit en SQL. En fait, de nombreuses fonctions LINQ ne sont pas prises en charge par Entity Framework. Voir Méthodes LINQ prises en charge et non prises en charge (LINQ vers les entités)

La solution appropriée serait de traduire votre fonction locale en une concaténation d'expressions LINQ prises en charge. Si vous ne pouvez pas faire cela, vous devrez d'abord exécuter la partie de la requête avant CalculateValueAddedTax , ce qui en fait un objet énumérable local, avant de pouvoir l'utiliser.

IQueryable<decimal> CalculateValueAddedTaxes(this IQueryable<Price> prices)
{
    return prices.Select(price => price.VatPercentage * prive.Value);
}

Bien que cela fonctionne, il serait inutile de récupérer toutes les données si vous prévoyez d'en utiliser seulement quelques-unes, par exemple si vous FirstOrDefault()

Price result = orderLines
    .Where(orderLine => orderLine.HasValueAddedTax)
    .Select(orderLine => orderLine.Price)
    .OrderByDescending(price => price.Value)
    .AsEnumerable()
    .Select(price => CaculateValueAddedTax(price))
    .FirstOrDefault();

Ce serait un gaspillage de transférer tous les humains américains, et de ne prendre que le premier. D'où l'invention d' AsEnumerable .

IQueryable<Human> queryAmericans = myDbContext.Humans
    .Where(human => human.Country == "USA")
    .OrderByDescending(human => human.Age);
List<Human> americans = queryAmericans.ToList();
var oldestSpecialAmerican = americans
    .Where(american => american.IsSpecial())
    .FirstOrDefault();

Cela dépend un peu du Provider , de ce AsEnumerable fera AsEnumerable , mais un Provider intelligent interrogera les données "par page", de sorte que tous les millions de données interrogées ne soient pas récupérées, mais seulement certaines, disons 25. Si vous utiliseriez FirstOrDefault , alors il y en aurait pour rien, mais du moins pas des millions. Dès que vous énumérez le 26e élément, la page suivante est récupérée. La taille de la page est un compromis entre avoir récupéré trop de données et devoir exécuter une requête souvent.

Ainsi, la principale différence entre AsEnumerable et AsQueryable est qu'un Enumerable sera exécuté par votre processus local: chaque fonction que vous pouvez appeler peut être exécutée par Enumerable. AsQueryable est exécuté par un processus externe. Chaque fonction que vous appelez doit être traduite dans la langue que le processus externe comprend, limitant ainsi les fonctions que vous pouvez utiliser dans votre requête.

Les compilateurs ne peuvent pas détecter la langue utilisée par le processus externe et ne se plaindront pas. Si vous utilisez des fonctions non prises en charge, vous obtiendrez une exception.

Retour à votre question: devrais-je utiliser AsEnumerable / AsQueryable

AsEnumerable

Les exemples ci-dessus montrent que vous devrez parfois interroger une partie des données vers le processus local avant de continuer LINQ-ing. La façon intelligente de faire cela consiste à utiliser AsEnumerable.

L'une des parties les plus lentes d'une requête de base de données est le transport des données sélectionnées vers votre processus local. Donc, si vous décidez d'utiliser AsEnumerable, essayez de ne transférer que les données dont vous avez besoin localement: ne transférez pas les OrderLines complètes si vous souhaitez uniquement traiter les Prices

En outre, voyez si vous pouvez changer votre fonction locale en un IQueryable.

IQueryable<OrderLine> orderLines = ...
IEnumerable<Price> pricesWithValueAddedTaxes = orderLines
    .Where(orderLine => orderLine.HasValueAddedTax)
    .Select(orderLine => orderLine.Price)
    .ToList();
decimal totalValueAddedTax = pricesWithValueAddedTax
    .Select(price => CaculateValueAddedTax(price))
    .Sum();

Désormais, la requête complète peut être exécutée par le système de gestion de base de données:

IQueryable<OrderLine> orderLines = ...
decimal totalValueAddedTax = orderLines
    .Where(orderLine => orderLine.HasValueAddedTax)
    .Select(orderLine => CaculateValueAddedTax(orderLine.Price))
    .Sum();

AsQueryable

Si vous avez un énumérable local, AsQueryable ne transférera pas soudainement vos données vers une base de données. Ce qu'il fait, il crée une Expression et un Provider . Cette Expression est uniquement l'appel de fonction aux données d'entrée. Comme avec n'importe quel objet interrogeable, lorsque vous commencez à énumérer, l' Expression est envoyée au Provider . Ce Provider ne fera rien de plus que d'exécuter l' Expression , ce qui signifie que GetEnumerator est appelé.

Alors pourquoi devrais-je utiliser AsQueryable?

Dans de rares occasions, vous aurez une séquence énumérable locale et vous devrez appeler une fonction qui a besoin d'un IQueryable comme entrée:

double CalculateValueAddedTax(Price p) {...}

IEnumerable<OrderLine> orderLines = ...
decimal totalValueAddedTax = orderLines
    .Where(orderLine => orderLine.HasValueAddedTax)
    .Select(orderLine => CaculateValueAddedTax(orderLine.Price))
    .Sum();


0 commentaires

-1
votes

J'ai réalisé qu'en combinant IEnumerable et IQueryable avec join:

  1. si le premier terme est IEnumerable et le second IQueryable , alors toute la requête serait IEnumerable , la partie IQueryable s'exécute d'abord et renvoie les données en mémoire, puis la requête continue.

  2. mais si le premier terme est IQueryable et le second IEnumerable , une exception sera levée car le deuxième terme n'a pas pu être traduit en IQueryable


0 commentaires