LINQ to Entities - where..in предложение с несколькими столбцами

Я пытаюсь запросить данные формы с помощью LINQ-to-EF:

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

просмотрев местоположение по кортежу (Страна, Город, Адрес). Я попробовал

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
             }

но LINQ не хочет принимать анонимный тип (который, как я понимаю, способ выразить кортежи в LINQ) в качестве параметра Contains().

Есть ли "хороший" способ выразить это в LINQ, имея возможность запускать запрос в базе данных? В качестве альтернативы, если бы я просто перебирал ключи и Union() - задавал вопросы вместе, было бы плохо для производительности?

Ответы

Ответ 1

Как насчет:

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

UPDATE

К сожалению, EF выбрасывает NotSupportedException, что лишает этого ответа, если вам нужен запрос для выполнения на стороне БД.

ОБНОВЛЕНИЕ 2

Пробовал все виды соединений с использованием пользовательских классов и кортежей - не работает. О каких объемах данных мы говорим? Если он не слишком велик, вы можете либо обработать его на стороне клиента (удобно), либо использовать союзы (если не быстрее, по крайней мере, меньше данных передается).

Ответ 2

Мое решение - создать новый метод расширения WhereOr, который использует ExpressionVisitor для построения запроса:

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;
    }
}

Ниже генерируется чистый код sql, выполняемый в базе данных:

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

Ответ 3

Хотя я не мог заставить код @YvesDarmaillac работать, он указал мне на это решение.

Вы можете создать выражение, а затем добавить каждое условие отдельно. Для этого вы можете использовать Universal PredicateBuilder (источник в конце).

Здесь мой код:

// 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);
}

Одна вещь, остерегаться, однако, состоит в том, что список фильтров (переменная keys в этом примере) не может быть слишком большим или вы можете достичь предела параметров, за исключением следующего:

SqlException: входящий запрос имеет слишком много параметров. Сервер поддерживает максимум 2100 параметров. Уменьшите количество параметров и повторите запрос.

Итак, в этом примере (с тремя параметрами в строке) вы не можете иметь более 700 локаций для фильтрации.

Используя два элемента для фильтрации, он будет генерировать 6 параметров в последнем SQL. Сгенерированный SQL будет выглядеть ниже (форматируется для более четкого):

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'

Обратите внимание, как исходное "ложное" выражение должным образом игнорируется и не включено в окончательный SQL по EntityFramework.

Наконец, здесь для Universal PredicateBuilder для записи.

/// <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);
        }
    }
}

Ответ 4

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

должен быть:

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

Ответ 5

Пробовал ли вы просто использовать класс 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))

Ответ 6

Если вам не понадобится много комбинаций клавиш, вы можете просто добавить свойство LocationKey к своим данным. Чтобы не тратить много средств на хранение, возможно, это хэш-код комбинированных свойств.

Тогда запрос будет просто иметь условие на LocationKey. Наконец, на стороне клиента фильтруйте результаты, чтобы удалить объекты, которые имеют одинаковый хэш, но не одно и то же местоположение.

Он будет выглядеть примерно так:

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;
    …
}

Затем просто запросите свойство LocationKey.

Не идеален, но он должен работать.

Ответ 7

Я не думаю, что это сработает для вас с тех пор, как вы создадите объект в методе Contains, он будет каждый раз создавать новый объект. Поскольку этот объект является анонимным, то, как они будут сравниваться, не соответствует их ссылке, которая будет отличаться для каждого объекта.

Кроме того, посмотрите на ответ Яцека.

Ответ 8

    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

Попробуйте.

Ответ 9

Я думаю, что правильный способ сделать это

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

Он выглядит неоптимизированным, но поставщик запросов выйдет и сделает оптимизацию, когда он преобразует запрос в sql. При использовании кортежей или других классов поставщик запроса не знает, как преобразовать их в sql и что вызывает NotSupportedException

-edit -

Если у вас несколько ключевых кортежей, я думаю, вам нужно их пропустить и выполнить описанный выше запрос для каждого из них. опять же, это может показаться недооцененным, но запрос для извлечения всех местоположений в одном запросе, вероятно, закончится довольно долго:

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

Самый быстрый способ сделать это, вероятно, сделать простые запросы, но отправить их как один sql script и использовать несколько наборов результатов для фактического получения каждого значения. Я не уверен, что вы можете заставить EF сделать это, хотя.

Ответ 10

Я бы заменил Contains (это метод, специфичный для списков и массивов), с более широким IEnumerable. Любой метод расширения:

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

Это также можно записать:

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;