web-dev-qa-db-fra.com

GroupBy côté client n'est pas pris en charge

J'ai la requête Entity Framework Core 3.0 suivante:

var units = await context.Units
  .SelectMany(y => y.UnitsI18N)
  .OrderBy(y => y.Name)
  .GroupBy(y => y.LanguageCode)
  .ToDictionaryAsync(y => y.Key, y => y.Select(z => z.Name));

J'obtiens l'erreur suivante:

Client side GroupBy is not supported.

Pour exécuter la requête sur le client, ou une partie de celui-ci, je ferais ce qui suit:

var units = context.Units
  .SelectMany(y => y.UnitsI18N)
  .OrderBy(y => y.Name)
  .AsEnumerable()
  .GroupBy(y => y.LanguageCode)
  .ToDictionary(y => y.Key, y => y.Select(z => z.Name));

Maintenant ça marche.

Pourquoi est-ce que j'obtiens cette erreur si je n'exécute pas la requête sur le client?

28
Miguel Moura

Votre .GroupBy(y => y.LanguageCode).ToDictionaryAsync(y => y.Key, y => y.Select(z => z.Name)); ne peut pas être convertie en SQL. EF Core 3.0 lèvera une exception pour vous assurer que vous savez que tous les enregistrements de Units seront récupérés de la base de données avant de les regrouper et de les mapper au dictionnaire.

C'est le changement le plus révolutionnaire dans EF Core 3.0. https://docs.Microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes

15
Thanh Nguyen

Il semble qu'il y ait une idée fausse commune sur ce que fait LINQ GroupBy et ce que SQL GROUP BY Est capable de faire. Étant donné que je suis tombé dans le même piège et que j'ai dû en finir récemment avec la tête, j'ai décidé d'écrire une explication plus approfondie de ce problème.


Réponse courte:

LINQ GroupBy est très différent de l'instruction SQL GROUP BY: LINQ juste divise la collection sous-jacente en morceaux en fonction d'une clé, tandis que SQL ajoute une fonction d'agrégation pour condenser chacune des ces morceaux vers le bas dans un valeur unique.

C'est pourquoi EF doit exécuter votre type LINQ GroupBy en mémoire.

Avant EF Core 3.0, cela se faisait implicitement, donc EF a téléchargé toutes les lignes de résultats et a ensuite appliqué LINQ GroupBy. Cependant, ce comportement implicite peut laisser le programmeur s'attendre à ce que la requête entier LINQ soit exécutée en SQL, avec un impact potentiellement énorme sur les performances lorsque l'ensemble de résultats est assez volumineux. Pour cette raison, l'évaluation implicite côté client de GroupBy était complètement désactivée dans EF Core 3. .

Désormais, il est nécessaire d'appeler explicitement des fonctions telles que .AsEnumerable() ou .ToList(), qui téléchargent le jeu de résultats et poursuivent les opérations LINQ en mémoire.


Réponse longue:

Le tableau suivant solvedExercises sera l'exemple en cours d'exécution de cette réponse:

+-----------+------------+
| StudentId | ExerciseId |
+-----------+------------+
|         1 |          1 |
|         1 |          2 |
|         2 |          2 |
|         3 |          1 |
|         3 |          2 |
|         3 |          3 |
+-----------+------------+

Un enregistrement X | Y Dans ce tableau indique que l'élève X a résolu l'exercice Y.

Dans la question, un cas d'utilisation courant de la méthode GroupBy de LINQ est décrit: Prenez une collection et groupez-la en morceaux, où les lignes de chaque morceau partagent une clé commune.

Dans notre exemple, nous pourrions vouloir obtenir un Dictionary<int, List<int>>, Qui contient une liste d'exercices résolus pour chaque élève. Avec LINQ, c'est très simple:

var result = solvedExercises
    .GroupBy(e => e.StudentId)
    .ToDictionary(e => e.Key, e => e.Select(e2 => e2.ExerciseId).ToList());

Sortie (pour le code complet, voir dotnetfiddle ):

Student #1: 1 2 
Student #2: 2 
Student #3: 1 2 3 

C'est facile à représenter avec les types de données C #, car nous pouvons imbriquer List et Dictionary aussi profondément que nous le souhaitons.

Maintenant, nous essayons d'imaginer cela comme un résultat de requête SQL. Les résultats des requêtes SQL sont généralement représentés sous forme de tableau, où nous pouvons librement choisir les colonnes retournées. Pour représenter notre requête ci-dessus en tant que résultat de requête SQL, nous devons

  • générer plusieurs tableaux de résultats,
  • mettre les lignes groupées dans un tableau ou
  • insérez en quelque sorte un "séparateur de jeu de résultats".

Pour autant que je sache, aucune de ces approches n'est mise en œuvre dans la pratique. Tout au plus, il existe des solutions de contournement comme le MySQL GROUP_CONCAT , qui permet de combiner les lignes de résultat en une chaîne ( réponse SO pertinente) ).

Ainsi, nous voyons que SQL ne peut pas produire des résultats qui correspondent à la notion de LINQ de GroupBy.

Au lieu de cela, SQL n'autorise que les soi-disant agrégation: Si nous, par exemple, voulions compter le nombre d'exercices passés par un étudiant, nous écririons

SELECT StudentId,COUNT(ExerciseId)
FROM solvedExercises
GROUP BY StudentId

... qui donnera

+-----------+-------------------+
| StudentId | COUNT(ExerciseId) |
+-----------+-------------------+
|         1 |                 2 |
|         2 |                 1 |
|         3 |                 3 |
+-----------+-------------------+

Les fonctions d'agrégation réduisent un ensemble de lignes en une seule valeur, généralement un scalaire. Les exemples sont le nombre de lignes, la somme, la valeur maximale, la valeur minimale et la moyenne.

This is implémenté par EF Core: Exécution

var result = solvedExercises
    .GroupBy(e => e.StudentId)
    .Select(e => new { e.Key, Count = e.Count() })
    .ToDictionary(e => e.Key, e => e.Count);

génère le SQL ci-dessus. Notez le Select, qui indique à EF quelle fonction d'agrégation il doit utiliser pour la requête SQL générée.


En résumé, la fonction LINQ GroupBy est beaucoup plus générale que l'instruction SQL GROUP BY, Qui, en raison des restrictions de SQL, ne permet de renvoyer qu'une seule table de résultats bidimensionnelle. Ainsi, les requêtes comme celle de la question et le premier exemple de cette réponse doivent être évalués en mémoire, après avoir téléchargé le jeu de résultats SQL.

Au lieu de implicitement faire cela, dans EF Core 3.0 les développeurs ont choisi de lever une exception dans ce cas ; cela empêche le téléchargement accidentel d'une table entière, potentiellement volumineuse avec des millions de lignes, qui pourrait passer inaperçue pendant le développement en raison d'une petite base de données de test.

2
Jan Wichelmann