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

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

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

путем поиска местоположения по кортежу (страна, город, адрес). Я пытался!--3-->

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

11 ответов


Как насчет:

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

обновление

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

обновление 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
                                          )
                          );

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

вы можете создать выражение, а затем добавить каждое условие отдельно. Для этого можно использовать универсальный 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 будет выглядеть следующим образом (отформатирован для clearer):

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.

наконец, вот код Универсальный 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);
        }
    }
}

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;

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

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

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

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

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

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


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

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


    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

попробуйте.


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

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


Я бы заменил 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;