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?
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
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
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.