web-dev-qa-db-fra.com

EF: inclure avec la clause where

Comme le titre l'indique, je cherche un moyen de faire une clause where en combinaison avec une inclusion.

Voici mes situations: je suis responsable du support d'une grande application pleine d'odeurs de code. Changer trop de code provoque des bugs partout, donc je cherche la solution la plus sûre.

Disons que j'ai un objet Bus et un objet People (Bus a un accessoire de navigation Collection of People). Dans ma requête, je dois sélectionner tous les bus avec uniquement les passagers qui sont éveillés. Ceci est un exemple factice simpliste

Dans le code actuel:

var busses = Context.Busses.Where(b=>b.IsDriving == true);
foreach(var bus in busses)
{
   var passengers = Context.People.Where(p=>p.BusId == bus.Id && p.Awake == true);
   foreach(var person in passengers)
   {
       bus.Passengers.Add(person);
   }
}

Après ce code, le contexte est supprimé et dans la méthode d'appel, les entités de bus résultantes sont mappées à une classe DTO (copie à 100% de l'entité).

Ce code provoque plusieurs appels à la base de données, ce qui est un no-go, j'ai donc trouvé cette solution ON MSDN Blogs

Cela a très bien fonctionné lors du débogage du résultat, mais lorsque les entités sont mappées au DTO (à l'aide d'AutoMapper), j'obtiens une exception indiquant que le contexte/connexion a été fermé et que l'objet ne peut pas être chargé. (Le contexte est toujours fermé ne peut pas changer cela :()

Je dois donc m'assurer que les passagers sélectionnés sont déjà chargés (la propriété IsLoaded on navigation est également False). Si j'inspecte la collection Passagers, The Count lève également l'exception, mais il existe également une collection sur la collection de Passegers appelée "entités liées enveloppées" qui contient mes objets filtrés.

Existe-t-il un moyen de charger ces entités liées enveloppées dans toute la collection? (Je ne peux pas changer la configuration du mappage automapper car elle est utilisée dans toute l'application).

Existe-t-il un autre moyen d'obtenir les passagers actifs?

Tout indice est le bienvenu ...

Modifier

Réponse de Gert Arnold ne fonctionne pas car les données ne sont pas chargées avec impatience. Mais quand je le simplifie et supprime l'endroit où il est chargé. C'est vraiment étrange car le sql d'exécution renvoie tous les passagers dans les deux cas. Il doit donc y avoir un problème lors de la remise des résultats dans l'entité.

Context.Configuration.LazyLoadingEnabled = false;
var buses = Context.Busses.Where(b => b.IsDriving)
        .Select(b => new 
                     { 
                         b,
                         Passengers = b.Passengers
                     })
        .ToList()
        .Select(x => x.b)
        .ToList();

Modifier2

Après beaucoup de lutte, la réponse du travail de Gert Arnold! Comme l'a suggéré Gert Arnold, vous devez désactiver le chargement paresseux et le garder désactivé. Cela demandera des modifications supplémentaires à l'appliaction car le développeur précédent aimait Lazy Loading -_-

40
Beejee

Vous pouvez interroger les objets requis en

Context.Configuration.LazyLoadingEnabled = false;
// Or: Context.Configuration.ProxyCreationEnabled = false;
var buses = Context.Busses.Where(b => b.IsDriving)
            .Select(b => new 
                         { 
                             b,
                             Passengers = b.Passengers
                                           .Where(p => p.Awake)
                         })
            .AsEnumerable()
            .Select(x => x.b)
            .ToList();

Ce qui se passe ici, c'est que vous allez d'abord chercher les bus qui conduisent et réveillez les passagers de la base de données. Ensuite, AsEnumerable() passe de LINQ à Entités à LINQ à objets, ce qui signifie que les bus et les passagers seront matérialisés puis traités en mémoire. Ceci est important car sans lui, EF ne matérialisera que la projection finale, Select(x => x.b), pas les passagers.

Maintenant EF a cette fonctionnalité correction de relation qui s'occupe de définir toutes les associations entre les objets matérialisés dans le contexte. Cela signifie que pour chaque Bus maintenant seuls ses passagers éveillés sont chargés.

Lorsque vous obtenez la collection de bus par ToList vous avez les bus avec les passagers que vous voulez et vous pouvez les cartographier avec AutoMapper.

Cela ne fonctionne que lorsque le chargement différé est désactivé. Sinon, EF chargera paresseusement tous passagers pour chaque bus lors de l'accès aux passagers pendant la conversion en DTO.

Il existe deux façons de désactiver le chargement paresseux. La désactivation de LazyLoadingEnabled réactivera le chargement différé lorsqu'il sera réactivé. La désactivation de ProxyCreationEnabled créera des entités qui ne sont pas capables de chargement paresseux elles-mêmes, donc elles ne recommenceront pas le chargement paresseux après que ProxyCreationEnabled soit à nouveau activé. Cela peut être le meilleur choix lorsque le contexte vit plus longtemps que cette seule requête.

Mais ... plusieurs à plusieurs

Comme indiqué, cette solution de contournement repose sur la correction des relations. Cependant, comme expliqué ici par Slauma , la correction des relations ne fonctionne pas avec les associations plusieurs-à-plusieurs. Si Bus-Passenger est plusieurs-à-plusieurs, la seule chose que vous pouvez faire est de le réparer vous-même:

Context.Configuration.LazyLoadingEnabled = false;
// Or: Context.Configuration.ProxyCreationEnabled = false;
var bTemp = Context.Busses.Where(b => b.IsDriving)
            .Select(b => new 
                         { 
                             b,
                             Passengers = b.Passengers
                                           .Where(p => p.Awake)
                         })
            .ToList();
foreach(x in bTemp)
{
    x.b.Pasengers = x.Passengers;
}
var busses = bTemp.Select(x => x.b).ToList();

... et le tout devient encore moins attrayant.

Outils tiers

Il y a une bibliothèque, EntityFramework.DynamicFilters qui rend cela beaucoup plus facile. Il vous permet de définir des filtres globaux pour les entités, qui seront ensuite appliqués chaque fois que l'entité est interrogée. Dans votre cas, cela pourrait ressembler à:

modelBuilder.Filter("Awake", (Person p) => p.Awake, true);

Maintenant, si vous le faites ...

Context.Busses.Where(b => b.IsDriving)
       .Include(b => b.People)

... vous verrez que le filtre est appliqué à la collection incluse.

Vous pouvez également activer/désactiver les filtres afin de pouvoir contrôler le moment où ils sont appliqués. Je pense que c'est une bibliothèque très soignée.

Il existe une bibliothèque similaire du fabricant d'AutoMapper: EntityFramework.Filters

Noyau Entity Framework

Depuis la version 2.0.0, EF-core a filtres de requête globaux . Bien qu'il s'agisse d'un excellent ajout à ses fonctionnalités, jusqu'à présent, la limitation est qu'un filtre ne peut pas contenir de références aux propriétés de navigation, uniquement à l'entité racine d'une requête. Espérons que dans une version ultérieure, ces filtres atteindront une utilisation plus large.

Les inclusions filtrées sont une demande de fonctionnalité de longue date. Le problème EF-core peut être trouvé ici .

50
Gert Arnold

Avertissement : Je suis le propriétaire du projet Entity Framework Plus

La fonction EF + Query IncludeFilter permet de filtrer les entités liées.

var buses = Context.Busses
                   .Where(b => b.IsDriving)
                   .IncludeFilter(x => x.Passengers.Where(p => p.Awake))
                   .ToList();

Wiki: EF + Query IncludeFilter

19
Jonathan Magnan