LINQ to entities - où..dans la clause avec plusieurs colonnes

j'essaie D'interroger les données du formulaire avec LINQ-to-EF:

class Location {
    string Country;
    string City;
    string Address;
    …
}

en cherchant un lieu près du tuple (Pays, Ville, Adresse). J'ai essayé

var keys = new[] {
    new {Country=…, City=…, Address=…},
    …
}

var result = from loc in Location
             where keys.Contains(new {
                 Country=loc.Country, 
                 City=loc.City, 
                 Address=loc.Address
             }

mais LINQ ne veut pas accepter un type anonyme (ce que je comprends est la façon d'exprimer les tuples dans LINQ) comme paramètre à Contains ().

y a-t-il une "bonne" façon D'exprimer cela dans LINQ, tout en étant capable d'exécuter la requête sur la base de données? Alternativement, si je viens de parcourir les touches et Union()-ed les requêtes, ce serait mauvais pour la performance?

18
demandé sur millimoose 2011-08-02 17:19:12

11 réponses

Que Diriez-vous de:

var result = locations.Where(l => keys.Any(k => 
                    k.Country == l.Country && 
                    k.City == l.City && 
                    k.Address == l.Address));

UPDATE

malheureusement EF ne lance pas NotSupportedException sur cela, ce qui disqualifie cette réponse si vous avez besoin de la requête pour exécuter du côté de la base de données.

UPDATE 2

essayé toutes sortes de jointures à l'aide de classes personnalisées et Tuples - ni l'un ni l'autre ne fonctionne. Quels volumes de données parlons-nous? Si ce n'est pas trop grand, vous pouvez soit le traiter côté client (pratique) ou utiliser les syndicats (si ce n'est pas plus rapide, à moins de moins de données est transmis).

5
répondu Jacek Gorgoń 2011-08-08 07:49:44

ma solution est de construire une nouvelle méthode d'extension où ou qui utilise un ExpressionVisitor pour construire la requête:

public delegate Expression<Func<TSource, bool>> Predicat<TCle, TSource>(TCle cle);

public static class Extensions
{
    public static IQueryable<TSource> WhereOr<TSource, TCle>(this IQueryable<TSource> source, IEnumerable<TCle> cles, Predicat<TCle, TSource> predicat)
        where TCle : ICle,new()
    {
        Expression<Func<TSource, bool>> clause = null;

        foreach (var p in cles)
        {
            clause = BatisseurFiltre.Or<TSource>(clause, predicat(p));
        }

        return source.Where(clause);
    }
}

class BatisseurFiltre : ExpressionVisitor
{
    private ParameterExpression _Parametre;
    private BatisseurFiltre(ParameterExpression cle)
    {
        _Parametre = cle;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return _Parametre;
    }

    internal static Expression<Func<T, bool>> Or<T>(Expression<Func<T, bool>> e1, Expression<Func<T, bool>> e2)
    {
        Expression<Func<T, bool>> expression = null;

        if (e1 == null)
        {
            expression = e2;
        }
        else if (e2 == null)
        {
            expression = e1;
        }
        else
        {
            var visiteur = new BatisseurFiltre(e1.Parameters[0]);
            e2 = (Expression<Func<T, bool>>)visiteur.Visit(e2);

            var body = Expression.Or(e1.Body, e2.Body);
            expression = Expression.Lambda<Func<T, bool>>(body, e1.Parameters[0]);
        }

        return expression;
    }
}

ce qui suit génère du code sql propre exécuté sur la base de données:

var result = locations.WhereOr(keys, k => (l => k.Country == l.Country && 
                                                k.City == l.City && 
                                                k.Address == l.Address
                                          )
                          );
5
répondu Yves Darmaillac 2013-04-05 12:28:55

bien que je n'ai pas réussi à faire fonctionner le code de @YvesDarmaillac, il m'a indiqué cette solution.

vous pouvez construire une expression et ensuite ajouter chaque condition séparément. Pour ce faire, vous pouvez utiliser L'Universal PredicateBuilder (source à la fin).

Voici mon code:

// First we create an Expression. Since we can't create an empty one,
// we make it return false, since we'll connect the subsequent ones with "Or".
// The following could also be: Expression<Func<Location, bool>> condition = (x => false); 
// but this is clearer.
var condition = PredicateBuilder.Create<Location>(x => false);

foreach (var key in keys)
{
    // each one returns a new Expression
    condition = condition.Or(
        x => x.Country == key.Country && x.City == key.City && x.Address == key.Address
    );
}

using (var ctx = new MyContext())
{
    var locations = ctx.Locations.Where(condition);
}

une chose dont il faut se méfier, cependant, c'est que la liste de filtre (le keys variable dans cet exemple) ne peut pas être trop grand, ou vous pouvez atteindre la limite des paramètres, avec une exception comme ceci:

SqlException: la requête entrante a trop de paramètres. Le serveur supporte un maximum de 2100 paramètres. Réduire le nombre de paramètres et renvoyer la demande.

donc, dans cet exemple (avec trois paramètres par ligne), vous ne pouvez pas avoir plus de 700 emplacements à filtrer.

en utilisant deux éléments pour filtrer, il générera 6 Paramètres dans le SQL final. Le SQL généré ressemblera à ci-dessous (formaté pour être adhérent):

exec sp_executesql N'
SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Country] AS [Country], 
    [Extent1].[City] AS [City], 
    [Extent1].[Address] AS [Address]
FROM [dbo].[Locations] AS [Extent1]
WHERE 
    (
        (
            ([Extent1].[Country] = @p__linq__0) 
            OR 
            (([Extent1].[Country] IS NULL) AND (@p__linq__0 IS NULL))
        )
        AND 
        (
            ([Extent1].[City] = @p__linq__1) 
            OR 
            (([Extent1].[City] IS NULL) AND (@p__linq__1 IS NULL))
        ) 
        AND 
        (
            ([Extent1].[Address] = @p__linq__2) 
            OR 
            (([Extent1].[Address] IS NULL) AND (@p__linq__2 IS NULL))
        )
    )
    OR
    (
        (
            ([Extent1].[Country] = @p__linq__3) 
            OR 
            (([Extent1].[Country] IS NULL) AND (@p__linq__3 IS NULL))
        )
        AND 
        (
            ([Extent1].[City] = @p__linq__4) 
            OR 
            (([Extent1].[City] IS NULL) AND (@p__linq__4 IS NULL))
        ) 
        AND 
        (
            ([Extent1].[Address] = @p__linq__5) 
            OR 
            (([Extent1].[Address] IS NULL) AND (@p__linq__5 IS NULL))
        )
    )
',
N'
    @p__linq__0 nvarchar(4000),
    @p__linq__1 nvarchar(4000),
    @p__linq__2 nvarchar(4000),
    @p__linq__3 nvarchar(4000),
    @p__linq__4 nvarchar(4000),
    @p__linq__5 nvarchar(4000)
',
@p__linq__0=N'USA',
@p__linq__1=N'NY',
@p__linq__2=N'Add1',
@p__linq__3=N'UK',
@p__linq__4=N'London',
@p__linq__5=N'Add2'

remarquez comment l'expression "false" initiale est correctement ignorée et non incluse dans le SQL final par Entitefram Framework.

Enfin, voici le code Universal PredicateBuilder, pour mémoire.

/// <summary>
/// Enables the efficient, dynamic composition of query predicates.
/// </summary>
public static class PredicateBuilder
{
    /// <summary>
    /// Creates a predicate that evaluates to true.
    /// </summary>
    public static Expression<Func<T, bool>> True<T>() { return param => true; }

    /// <summary>
    /// Creates a predicate that evaluates to false.
    /// </summary>
    public static Expression<Func<T, bool>> False<T>() { return param => false; }

    /// <summary>
    /// Creates a predicate expression from the specified lambda expression.
    /// </summary>
    public static Expression<Func<T, bool>> Create<T>(Expression<Func<T, bool>> predicate) { return predicate; }

    /// <summary>
    /// Combines the first predicate with the second using the logical "and".
    /// </summary>
    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.AndAlso);
    }

    /// <summary>
    /// Combines the first predicate with the second using the logical "or".
    /// </summary>
    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.OrElse);
    }

    /// <summary>
    /// Negates the predicate.
    /// </summary>
    public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expression)
    {
        var negated = Expression.Not(expression.Body);
        return Expression.Lambda<Func<T, bool>>(negated, expression.Parameters);
    }

    /// <summary>
    /// Combines the first expression with the second using the specified merge function.
    /// </summary>
    static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
    {
        // zip parameters (map from parameters of second to parameters of first)
        var map = first.Parameters
            .Select((f, i) => new { f, s = second.Parameters[i] })
            .ToDictionary(p => p.s, p => p.f);

        // replace parameters in the second lambda expression with the parameters in the first
        var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);

        // create a merged lambda expression with parameters from the first expression
        return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
    }

    class ParameterRebinder : ExpressionVisitor
    {
        readonly Dictionary<ParameterExpression, ParameterExpression> map;

        ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
        {
            this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
        }

        public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
        {
            return new ParameterRebinder(map).Visit(exp);
        }

        protected override Expression VisitParameter(ParameterExpression p)
        {
            ParameterExpression replacement;

            if (map.TryGetValue(p, out replacement))
            {
                p = replacement;
            }

            return base.VisitParameter(p);
        }
    }
}
5
répondu Marcos Dimitrio 2017-05-11 04:39:13
var result = from loc in Location
             where keys.Contains(new {
                 Country=l.Country, 
                 City=l.City, 
                 Address=l.Address
             }

devrait être:

var result = from loc in Location
             where keys.Contains(new {
                 Country=loc.Country, 
                 City=loc.City, 
                 Address=loc.Address
             }
             select loc;
2
répondu Chris Snowden 2011-08-12 13:20:52

il existe une extension EF qui a été conçue pour un cas très similaire. C'est Entiteframeworkcore.MemoryJoin (le nom peut prêter à confusion, mais il supporte à la fois EF6 et EF Core). Comme indiqué dans l'auteur article il modifie la requête SQL passée au serveur et injecte valeurs construction avec des données de votre liste locale. Et la requête est exécutée sur le serveur de la base de données.

donc pour votre cas l'utilisation pourrait être comme ceci

var keys = new[] {
  new {Country=…, City=…, Address=…},
  …
}

// here is the important part!
var keysQueryable = context.FromLocalList(keys);

var result = from loc in Location
    join key in keysQueryable on new { loc.Country, loc.City, loc.Address } equals new { key.Country, key.City, key.Address }
    select loc
2
répondu Tony 2018-03-23 03:11:58

avez-vous essayé juste d'utiliser le cours de Tuple?

var keys = new[] {
    Tuple.Create("Country", "City", "Address"),
    …
}

var result = from loc in Location
             where keys.Contains(Tuple.Create(loc.Country, loc.City, loc.Address))
1
répondu sellmeadog 2011-08-02 15:24:08

Si vous n'allez pas avoir besoin de beaucoup de combinaisons de touches, vous pouvez simplement ajouter un LocationKey propriété de vos données. Pour éviter de gaspiller beaucoup de stockage, peut-être en faire le code de hachage des propriétés combinées.

requête sur auront simplement une condition sur LocationKey. Enfin, dans le filtre côté client les résultats pour supprimer les entités qui ont le même hachage mais pas le même emplacement.

Il ressemblerait à quelque chose comme:

class Location 
{
    private string country;
    public string Country
    {
        get { return country; }
        set { country = value; UpdateLocationKey(); }
    }

    private string city;
    public string City
    {
        get { return city; }
        set { city = value; UpdateLocationKey(); }
    }

    private string address;
    public string Address
    {
        get { return address; }
        set { address = value; UpdateLocationKey(); }
    }

    private void UpdateLocationKey()
    {
        LocationKey = Country.GetHashCode() ^ City.GetHashCode() ^ Address.GetHashCode();
    }

    int LocationKey;
    …
}

alors il suffit de faire une recherche sur le Propriété LocationKey.

pas idéal, mais ça devrait marcher.

1
répondu Ran 2011-08-12 07:50:46

je ne pense pas qui va travailler pour vous depuis quand vous êtes newing un objet dans le Contains méthode elle créera un nouvel objet à chaque fois. Puisque ces objets sont anonymes, la façon dont ils seront comparés sont contre leur référence qui sera différent pour chaque objet.

aussi, regardez la réponse de Jacek.

0
répondu Tomas Jansson 2011-08-02 13:29:33
    var keys = new[] {
        new {Country=…, City=…, Address=…},
        …
    }    
    var result = from loc in Location
                 where keys.Any(k=>k.Country == loc.Country 
&& k.City == loc.City 
&& k.Address == loc.Address) 
select loc

Donner à ceci un essai.

0
répondu AD.Net 2011-08-02 15:31:40

je pense que la bonne façon de le faire est de

var result = from loc in Location
             where loc.Country = _country
             where loc.City = _city
             where loc.Address = _address
             select loc

il semble non optimisé mais le fournisseur de requête va sortir et faire l'optimisation quand il transforme la requête en sql. Lors de l'utilisation de tuples ou d'autres classes, le fournisseur de requêtes ne sait pas comment les transformer en sql et que ce qui provoque la NotSupportedException

- edit -

si vous avez plusieurs tuples clés, je pense que vous devez les passer en boucle et faire la requête ci-dessus pour chacun d'eux. encore une fois, que peut sembler sous-optimisé, mais la requête pour retrouver tous les emplacements dans une seule requête finirait probablement par être assez long:

select * from locations 
where (locations.Country = @country1 and locations.City = @city1, locations.Adress = @adress1)
or (locations.Country = @country2 and locations.City = @city2, locations.Adress = @adress2)
or ...

la façon la plus rapide de le faire est probablement de faire les requêtes simples, mais de les envoyer comme un seul script sql et d'utiliser plusieurs ensembles de résultats pour obtenir réellement chaque valeur. Je ne suis pas sûr que vous pouvez obtenir EF pour le faire.

0
répondu aL3891 2011-08-11 08:33:24

je remplacerais Contains (qui est une méthode spécifique aux listes et aux tableaux) avec la plus large méthode D'extension de IEnumerable:

var result = Location
    .Where(l => keys.Any(k => l.Country == k.Country && l.City = k.City && l.Address == k.Address);

Cela peut aussi être écrit:

var result = from l in Location
             join k in keys
             on l.Country == k.Country && l.City == k.City && l.Address == k.Address
             select l;
-1
répondu Evren Kuzucuoglu 2011-08-12 10:44:29