web-dev-qa-db-ja.com

LINQ toEntities-複数の列を持つ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は、Contains()のパラメーターとして匿名型(LINQでタプルを表現する方法であると私は理解しています)を受け入れたくありません。

データベースでクエリを実行しながら、これをLINQで表現する「良い」方法はありますか?あるいは、キーを繰り返し処理し、Union()でクエリを一緒に実行した場合、パフォーマンスが低下しますか?

20
millimoose

どうですか:

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

[〜#〜] update [〜#〜]

残念ながら、EFはその上でNotSupportedExceptionをスローします。これにより、クエリをDB側で実行する必要がある場合、この回答は失格になります。

更新2

カスタムクラスとタプルを使用してすべての種類の結合を試しましたが、どちらも機能しません。どのデータ量について話しているのですか?大きすぎない場合は、クライアント側で処理するか(便利)、ユニオンを使用することができます(高速ではない場合でも、送信されるデータは少なくとも少なくなります)。

6
Jacek Gorgoń

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

ただし、注意すべき点の1つは、フィルターリスト(この例ではkeys変数)が大きすぎないことです。そうしないと、次のような例外を除いて、パラメーターの制限に達する可能性があります。

SqlException:着信リクエストのパラメータが多すぎます。サーバーは最大2100個のパラメーターをサポートします。パラメータの数を減らして、リクエストを再送信してください。

したがって、この例(1行に3つのパラメーターがある)では、フィルターする場所を700を超えることはできません。

2つの項目を使用してフィルタリングすると、最終的なSQLで6つのパラメーターが生成されます。生成された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」式が適切に無視され、EntityFrameworkによって最終的なSQLに含まれないことに注意してください。

最後に、レコードの niversal 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);
        }
    }
}
5
Marcos Dimitrio

私の解決策は、ExpressionVisitorを使用してクエリを作成する新しい拡張メソッドWhereOrを作成することです。

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
                                          )
                          );
5
Yves Darmaillac

非常によく似たケースに合わせて設計されたEF拡張機能があります。 EntityFrameworkCore.MemoryJoin (名前はわかりにくいかもしれませんが、EF6とEF Coreの両方をサポートしています)です。著者の 記事 で述べられているように、サーバーに渡されるSQLクエリを変更し、[〜#〜]値[〜#〜]を挿入しますローカルリストのデータを使用した構築。そして、クエリはDBサーバーで実行されます。

したがって、あなたの場合の使用法は次のようになります

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
Tony
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;
2
Chris Snowden

多くのキーの組み合わせが必要ない場合は、データに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
Ran

タプルクラスを使用してみましたか?

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

これを試してみてください。

0
AD.Net

文字列連結キーを射影し、射影に一致させることができます。ただし、列に作成されたインデックスは使用できず、文字列の一致が行われるため、処理が遅くなる可能性があることに注意してください。

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();
0
Mike

Containsメソッドでオブジェクトを新規作成すると、毎回新しいオブジェクトが作成されるため、これではうまくいかないと思います。これらのオブジェクトは匿名であるため、比較される方法は、オブジェクトごとに異なる参照とは異なります。

また、Jacekの答えを見てください。

0
Tomas Jansson

私はそれを行うための適切な方法は

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

最適化されていないように見えますが、クエリをsqlに変換すると、クエリプロバイダーが停止して最適化を実行します。タプルまたは他のクラスを使用する場合、クエリプロバイダーはそれらをSQLに変換する方法と、NotSupportedExceptionの原因を知りません。

-編集-

複数のキータプルがある場合は、それらすべてをループして、それぞれに対して上記のクエリを実行する必要があると思います。繰り返しになりますが、これは最適化されていないように見えるかもしれませんが、1つのクエリですべての場所を取得するためのクエリは、おそらく非常に長くなります。

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
aL3891