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


person millimoose    schedule 02.08.2011    source источник


Ответы (13)


Как насчет:

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

ОБНОВЛЕНИЕ

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

ОБНОВЛЕНИЕ 2

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

person Jacek Gorgoń    schedule 02.08.2011
comment
поскольку вопрос касается Linq to Entities, я сомневаюсь, что это сработает, в противном случае хорошее предложение. - person BrokenGlass; 02.08.2011
comment
Я проверяю это прямо сейчас, чтобы увидеть, понимает ли это EF. Другой ORM, который я использую, подойдет. - person Jacek Gorgoń; 02.08.2011
comment
Я соглашусь с этим как с подробным описанием, которое кажется невозможным в ответе LINQ-to-EF. Объем данных в моем случае невелик, поэтому я пошел с Union()-обработкой запросов вместе (поскольку динамическое построение предиката в LINQ болезненно) и скрестил пальцы, чтобы SQL Server мог вычислить все совпадения с одним и тем же индексом. - person millimoose; 12.08.2011

Мое решение состоит в том, чтобы создать новый метод расширения 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
                                          )
                          );
person Yves Darmaillac    schedule 05.04.2013
comment
Интересный подход, интересно, можно ли это реализовать с помощью LinqKit? - person millimoose; 05.04.2013
comment
Я пытался использовать ваше расширение, но ICle не определено. Можете ли вы включить определение для ICle? - person Grinn; 24.12.2014

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

Наконец, вот код для универсального конструктора предикатов, для записывать.

/// <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);
        }
    }
}
person Marcos Dimitrio    schedule 11.05.2017

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;
person Chris Snowden    schedule 02.08.2011
comment
Это была опечатка, которую я сделал, создавая упрощенный пример, я исправил вопрос. - person millimoose; 02.08.2011
comment
Я добавляю этот ответ, единственное, чего ему не хватает, - это строки выбора и конца запроса. - person Evren Kuzucuoglu; 12.08.2011

Существует расширение EF, которое было разработано для очень похожего случая. Это EntityFrameworkCore.MemoryJoin (название может сбивать с толку, но оно поддерживает как EF6, так и EF). Основной). Как указано в статье автора он изменяет 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
person Tony    schedule 23.03.2018
comment
Это выглядит многообещающе! К сожалению, я не могу на самом деле проверить, сработает ли это, потому что сейчас я работаю над совершенно другим проектом, но я буду помнить об этом, когда проблема повторится. - person millimoose; 24.03.2018

Вы пробовали просто использовать класс 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))
person sellmeadog    schedule 02.08.2011
comment
Это не компилируется с: Делегат «System.Func‹Location,int,bool›» не принимает 1 аргумент - person millimoose; 03.08.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.

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

person Ran    schedule 12.08.2011
comment
Схема базы данных, с которой я работаю, на самом деле имеет сопоставление компонентов местоположения с ключом в базе данных, и запрос, который я создаю, ищет их. Однако идея хешировать их вместе вместо хранения явного сопоставления хороша. - person millimoose; 12.08.2011

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

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

person Tomas Jansson    schedule 02.08.2011
comment
Там есть загвоздка. Согласно msdn.microsoft.com/en-us/library/bb397696.aspx два экземпляра одного и того же анонимного типа равны, только если все их свойства равны. это означает, что способ Криса тоже должен работать. - person Jacek Gorgoń; 02.08.2011
comment
@Thomas: Contains использует компаратор равенства, который для анонимных типов использует равенство свойств - это не проблема. - person BrokenGlass; 02.08.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

Попробуйте это.

person AD.Net    schedule 02.08.2011
comment
Я считаю, что это то же самое, что и ответ @Jacek, который не работает в LINQ-to-EF. - person millimoose; 04.08.2011

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

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

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

-редактировать-

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

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 сделать это.

person aL3891    schedule 11.08.2011
comment
да, создание полного запроса вместо использования подхода or было бы еще дольше, но можно было бы превратить короткий запрос в подготовленный оператор, и, таким образом, это было бы быстрее. я не уверен, что что-либо из этого поддерживается EF, хотя - person aL3891; 11.08.2011

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

var stringKeys = keys
    .Select(l => $"{l.Country}-{l.City}-{l.Address}")
    .ToList();

var result = locations
    .Select(l => new
    {
        Key = l.Country + "-" + l.City + "-" + l.Address)
    }
    .Where(l => stringKeys.Contains(l.Key))
    .ToList();
person Mike    schedule 13.09.2019

Как проверить, существует ли использование LINQ to SQL на основе нескольких столбцов

Учитывая:

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

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

Вы должны сделать что-то вроде этого:

from loc in Location where (
    from k in keys where k.Country==loc.Country && k.City==loc.City && k.Address=loc.Address select 1).Any()

Что приведет к следующему SQL:

FROM [Locations] AS [p0]
WHERE (NOT (EXISTS (
    SELECT 1
    FROM [Keys] AS [p1]
    WHERE [p0].[Country] = [p1].[Country]) AND ([p0].[City] = [p1].[City]) AND ([p0].[Address]=[p1].[Address])))
person Bryan S.    schedule 13.04.2021

Я бы заменил Содержит (метод, специфичный для списков и массивов) на более широкий метод расширения 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;
person Evren Kuzucuoglu    schedule 12.08.2011
comment
Я полагаю, что несколько человек уже дали этот ответ, который не работает в LINQ-to-EF. - person millimoose; 12.08.2011