9 votes

Tapez le support de membre dans LINQ-to-Entities?

Je dispose d'un projet MVC3 utilisant le modèle Entity Framework dans lequel j'ai marqué une classe comme ceci :

public partial class Product
{
    public bool IsShipped
    {
        get { /* do stuff */ }
    }
}

et que je veux utiliser dans une expression LINQ :

db.Products.Where(x => x.IsShipped).Select(...);

cependant, je reçois l'erreur suivante :

System.NotSupportedException a été interceptée par le code utilisateur Message=Le membre de type spécifié 'IsShipped' n'est pas pris en charge dans LINQ vers les entités. Seuls les initialiseurs, les membres d'entité et les propriétés de navigation d'entité sont pris en charge. Source=System.Data.Entity

J'ai cherché sur Google mais je n'ai rien trouvé de définitif sur cette utilisation donc j'ai essayé :

public partial class Product
{
    public bool IsShipped()
    {
        /* do stuff */
    }
}

db.Products.Where(x => x.IsShipped()).Select(...);

mais alors j'obtiens :

System.NotSupportedException a été interceptée par le code utilisateur Message=LINQ aux entités ne reconnaît pas la méthode 'Boolean IsShipped()' et cette méthode ne peut pas être traduite en une expression en mémoire.
Source=System.Data.Entity

il y a une fonctionnalité là que je ne veux pas intégrer dans la requête LINQ elle-même... quelle est une bonne façon de gérer cela ?

* mise à jour *

Darin fait valoir le point valide selon lequel tout ce qui est fait dans l'implémentation de IsShipped devrait être converti en une requête SQL et le compilateur ne sait probablement pas comment le faire, c'est pourquoi récupérer tous les objets en mémoire semble être le seul choix (à moins qu'une requête directe à la base de données soit faite). J'ai essayé comme ceci :

IEnumerable xp = db.Quizes
    .ToList()
    .Where(x => !x.IsShipped)
    .Select(x => x.Component.Product);

mais cela génère cette erreur :

Une violation de contrainte de multiplicité de relation s'est produite : Une EntityReference ne peut avoir qu'un seul objet associé, mais la requête a renvoyé plus d'un objet associé. Il s'agit d'une erreur irrécupérable.

bien que curieusement ceci fonctionne :

IEnumerable xp = db.Quizes
    .ToList()
    .Where(x => x.Skill.Id == 3)
    .Select(x => x.Component.Product);

pourquoi cela ?

* mise à jour II *

désolé, cette dernière déclaration ne fonctionne pas non plus...

* mise à jour III *

Je clos cette question en faveur de la poursuite d'une solution comme suggéré ici pour aplatir ma logique dans une requête - la discussion se déplacera vers ce nouveau poste. La deuxième alternative, de récupérer l'intégralité de la requête initiale en mémoire, est probablement inacceptable, mais la troisième, implémenter la logique comme une requête directe à la base de données, reste à explorer.

Merci à tous pour vos précieuses contributions.

13voto

Slauma Points 76561

La seule façon de rendre ceci "DRY" (éviter de répéter la logique à l'intérieur de IsShipped dans la clause Where à nouveau) et d'éviter de charger toutes les données en mémoire avant d'appliquer le filtre est d'extraire le contenu de IsShipped dans une expression. Vous pouvez ensuite utiliser cette expression en paramètre de Where et également dans IsShipped. Exemple :

public partial class Product
{
    public int ProductId { get; set; }           // <- mappé à la BD
    public DateTime? ShippingDate { get; set; }  // <- mappé à la BD
    public int ShippedQuantity { get; set; }     // <- mappé à la BD

    // Expression statique qui doit être comprise
    // par LINQ to Entities, c'est-à-dire traduisible en SQL
    public static Expression> IsShippedExpression
    {
        get { return p => p.ShippingDate.HasValue && p.ShippedQuantity > 0; }
    }

    public bool IsShipped // <- non mappé à la BD car en lecture seule
    {
        // Compiler l'expression en délégué Func
        // et l'exécuter
        get { return Product.IsShippedExpression.Compile()(this); }
    }
}

Ensuite, vous pouvez effectuer la requête de la manière suivante :

var result = db.Products.Where(Product.IsShippedExpression).Select(...).ToList();

Ici, vous auriez seulement un endroit où mettre la logique (IsShippedExpression) et l'utiliser pour les requêtes à la base de données et aussi dans votre propriété IsShipped.

Le ferais-je ? Dans la plupart des cas probablement non, car compiler l'expression est lent. À moins que la logique ne soit très complexe, susceptible de changer et que je sois dans une situation où les performances de l'utilisation de IsShipped n'ont pas d'importance, je répéterais la logique. Il est toujours possible d'extraire les filtres souvent utilisés dans une méthode d'extension :

public static class MyQueryExtensions
{
    public static IQueryable WhereIsShipped(
        this IQueryable query)
    {
        return query.Where(p => p.ShippingDate.HasValue && p.ShippedQuantity >0);
    }
}

Et ensuite l'utiliser de cette manière :

var result = db.Products.WhereIsShipped().Select(...).ToList();

Vous auriez alors deux endroits pour maintenir la logique : la propriété IsShipped et la méthode d'extension, mais vous pourrez la réutiliser.

1voto

adrift Points 24386

Je suppose que IsShipped n'est pas mappé à un champ dans la base de données? Cela expliquerait pourquoi Linq to Entities se plaint - il ne peut pas construire une instruction sql basée sur cette propriété.

Votre /* do stuff */ est-il basé sur des champs qui sont dans la base de données? Si c'est le cas, vous pourriez utiliser cette logique dans votre .Where().

0voto

Darin Dimitrov Points 528142

Vous pourriez d'abord consommer le résultat en appelant .ToList() puis effectuer le filtre côté client :

var result = db.Products.ToList().Where(x => x.IsShipped).Select(...);

Bien sûr, vous devez être conscient(e) qu'en faisant cela, vous risquez probablement de ralentir les performances de votre application car les bases de données le font mieux.

0voto

Merlyn Morgan-Graham Points 31815

il y a une fonctionnalité que je ne veux pas intégrer dans la requête LINQ elle-même... quelle est la meilleure façon de gérer cela?

Je suppose que vous voulez effectuer des requêtes qui n'ont rien à voir avec la base de données. Mais votre code ne correspond pas à votre intention. Regardez cette ligne:

db.Products.Where(x => x.IsShipped()).Select(...);

La partie qui dit db.Products signifie que vous voulez interroger la base de données.

Pour corriger cela, obtenez d'abord un ensemble d'entités en mémoire. Ensuite, vous pouvez utiliser Linq sur ces objets à la place :

List products = db.Products
    .Where(x => x.SomeDbField == someValue)
    .ToList();

// À faire : Puisque la base de données ne connaît pas IsShipped, définissez cette information ici

// ...

var shippedProducts = products
    .Where(x => x.IsShipped())
    .Select(...);

Le .ToList() finalise votre requête initiale sur la base de données et vous donne une représentation en mémoire sur laquelle travailler et que vous pouvez modifier à votre guise. À partir de ce moment, vous pouvez travailler avec des propriétés non liées à la base de données.

Soyez prudent, si vous effectuez d'autres opérations sur la base de données après ToList (comme modifier des propriétés de la base de données sur les entités, interroger des propriétés de navigation, etc.), alors vous serez de nouveau dans le domaine de Linq to Entities et ne pourrez plus effectuer d'opérations de Linq sur des objets. Vous ne pouvez pas mélanger directement les deux.

Et notez que si public bool IsShipped() lit ou écrit des propriétés de la base de données ou des propriétés de navigation, vous pourriez vous retrouver à nouveau dans Linq to Entities si vous n'êtes pas prudent.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X