web-dev-qa-db-fra.com

Résultats de sélection Linq imbriqués EF Core dans N + 1 requêtes SQL

J'ai un modèle de données où un objet 'Top' a entre 0 et N 'objets' Sub '. En SQL, ceci est réalisé avec une clé étrangère dbo.Sub.TopId.

var query = context.Top
    //.Include(t => t.Sub) Doesn't seem to do anything
    .Select(t => new {
        prop1 = t.C1,
        prop2 = t.Sub.Select(s => new {
            prop21 = s.C3 //C3 is a column in the table 'Sub'
        })
        //.ToArray() results in N + 1 queries
    });
var res = query.ToArray();

Dans Entity Framework 6 (avec le chargement différé), cette requête Linq serait convertie en une requête SQL single. Le résultat serait entièrement chargé, donc res[0].prop2 serait un IEnumerable<SomeAnonymousType> déjà rempli.

Lors de l'utilisation de EntityFrameworkCore (NuGet v1.1.0), la sous-collection n'est pas encore chargée et est du type:

System.Linq.Enumerable.WhereSelectEnumerableIterator<Microsoft.EntityFrameworkCore.Storage.ValueBuffer, <>f__AnonymousType1<string>>.

Les données ne seront pas chargées jusqu'à ce que vous les parcouriez, ce qui entraîne N + 1 requêtes. Lorsque j'ajoute .ToArray() à la requête (comme indiqué dans les commentaires), les données sont entièrement chargées dans var res, mais l'utilisation d'un profileur SQL indique toutefois que cela n'est plus possible dans une requête SQL. Pour chaque objet "Top", une requête sur la table "Sub" est exécutée.

D'abord, spécifier .Include(t => t.Sub) ne semble rien changer. L'utilisation de types anonymes ne semble pas être le problème non plus, remplacer les blocs new { ... } par new MyPocoClass { ... } ne change rien.

Ma question est la suivante: Existe-t-il un moyen d'obtenir un comportement similaire à EF6, où toutes les données sont chargées immédiatement?


Note: Je réalise que, dans cet exemple, le problème peut être résolu en créant les objets anonymes en mémoire après en exécutant la requête comme suit:

var query2 = context.Top
    .Include(t => t.Sub)
    .ToArray()
    .Select(t => new //... select what is needed, fill anonymous types

Cependant, il ne s'agit que d'un exemple, j'ai besoin de la création d'objets pour faire partie de la requête Linq, car AutoMapper l'utilise pour remplir les DTO de mon projet.


Mise à jour: Testé avec le nouvel EF Core 2.0, le problème est toujours présent. (21-08-2017)

Le problème est suivi dans le aspnet/EntityFrameworkCore GitHub repo: Numéro 4007 </ S>.

Mise à jour: Un an plus tard, ce problème a été corrigé dans la version 2.1.0-preview1-final. (2018-03-01)

Mise à jour: EF version 2.1 a été publiée, elle inclut un correctif. voir ma réponse ci-dessous. (2018-05-31)

16
GWigWam

Le numéro de GitHub # 4007 a été marqué comme closed-fixed pour le jalon 2.1.0-preview1. Et maintenant, la version 2.1 de preview1 est disponible sur NuGet comme indiqué dans cet article .NET Blog post .

La version 2.1 proprement dite est également publiée, installez-la avec la commande suivante:

Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0

Utilisez ensuite .ToList() sur la .Select(x => ...) imbriquée pour indiquer que le résultat doit être extrait immédiatement. Pour ma question initiale, cela ressemble à ceci:

var query = context.Top
    .Select(t => new {
        prop1 = t.C1,
        prop2 = t.Sub.Select(s => new {
            prop21 = s.C3
        })
        .ToList() // <-- Add this
    });
var res = query.ToArray(); // Execute the Linq query

Cela entraîne l'exécution de 2 requêtes SQL sur la base de données (au lieu de N + 1); Tout d'abord, une table SELECTFROMlaine dans la table 'Top', puis une table SELECTFROM_la 'Sous' avec une table INNER JOINFROM'la 'table', basée sur la relation clé-clé étrangère [Sub].[TopId] = [Top].[Id]. Les résultats de ces requêtes sont ensuite combinés en mémoire.

Le résultat est exactement ce que vous attendiez et très similaire à ce que EF6 aurait renvoyé: Un tableau de type anonyme 'a qui a les propriétés prop1 et prop2prop2 est une liste de type anonyme 'b qui a une propriété prop21. Plus important encore tout cela est complètement chargé après l'appel .ToArray()!

7
GWigWam

J'ai rencontré le même problème.

La solution que vous avez proposée ne fonctionne pas pour des tables relativement grandes. Si vous examinez la requête générée, il s'agira d'une jointure interne sans condition where.

var query2 = context.Top .Include (t => t.Sub) .ToArray () .Select (t => new // ... sélectionne ce qui est nécessaire, remplit les types anonymes

Je l'ai résolu avec la refonte de la base de données, même si je serais heureux d'entendre une meilleure solution.

Dans mon cas, j’ai deux tables A et B. La table A contient un-à-plusieurs avec B. Lorsque j’essayais de la résoudre directement avec une liste comme vous l’aviez décrite, je n’étais pas parvenu à le faire LINQ était de 0,5 seconde, alors que .NET Core LINQ a échoué après 30 secondes de temps d'exécution).

En conséquence, j'ai dû créer une clé étrangère pour la table B et partir du côté de la table B sans liste interne.

context.A.Where(a => a.B.ID == 1).ToArray();

Ensuite, vous pouvez simplement manipuler les objets .NET obtenus.

1
Simon S