LINQ to Entities - предложение where..in с несколькими столбцами - PullRequest
19 голосов
/ 02 августа 2011

Я пытаюсь запросить данные формы с помощью 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 () - отредактировал запросы, это было бы плохо для производительности?

Ответы [ 11 ]

6 голосов
/ 11 мая 2017

Хотя я не смог заставить работать код @ 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'

Обратите внимание, как начальное выражение "false" правильно игнорируется и не включается в окончательный 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);
        }
    }
}
6 голосов
/ 02 августа 2011

Как насчет:

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

ОБНОВЛЕНИЕ

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

ОБНОВЛЕНИЕ 2

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

5 голосов
/ 05 апреля 2013

Мое решение заключается в создании нового метода расширения 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
                                          )
                          );
2 голосов
/ 23 марта 2018

Существует расширение EF, которое было разработано для очень похожего случая.Это EntityFrameworkCore.MemoryJoin (имя может сбивать с толку, но оно поддерживает EF6 и EF Core).Как указано в статье автора , он изменяет запрос SQL, передаваемый на сервер, и внедряет конструкцию VALUES с данными из вашего локального списка.И запрос выполняется на сервере БД.

Так что для вашего случая использование может быть таким:

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 голосов
/ 02 августа 2011
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;
1 голос
/ 12 августа 2011

Если вам не нужно много комбинаций клавиш, вы можете просто добавить свойство 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свойство.

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

1 голос
/ 02 августа 2011

Вы пробовали просто использовать класс 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))
0 голосов
/ 12 августа 2011

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

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;
0 голосов
/ 11 августа 2011

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

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 и используйте несколько наборов результатов для фактического получения каждого значения.Я не уверен, что вы можете заставить EF сделать это.

0 голосов
/ 02 августа 2011
    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

Попробуйте.

...