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() – 一起编写查询,这会对性能有害吗?

怎么样:

 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

尝试使用自定义类和元组的各种连接 – 两者都不起作用。 我们在谈论什么数据量? 如果它不是太大,你可以在客户端处理(方便)或使用联合(如果不是更快,至少传输的数据更少)。

我的解决方案是构建一个新的扩展方法WhereOr,它使用ExpressionVisitor来构建查询:

 public delegate Expression> Predicat(TCle cle); public static class Extensions { public static IQueryable WhereOr(this IQueryable source, IEnumerable cles, Predicat predicat) where TCle : ICle,new() { Expression> clause = null; foreach (var p in cles) { clause = BatisseurFiltre.Or(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> Or(Expression> e1, Expression> e2) { Expression> expression = null; if (e1 == null) { expression = e2; } else if (e2 == null) { expression = e1; } else { var visiteur = new BatisseurFiltre(e1.Parameters[0]); e2 = (Expression>)visiteur.Visit(e2); var body = Expression.Or(e1.Body, e2.Body); expression = Expression.Lambda>(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的代码,但它指出了这个解决方案。

您可以构建表达式,然后单独添加每个条件。 为此,您可以使用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> condition = (x => false); // but this is clearer. var condition = PredicateBuilder.Create(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); } 

但要注意的一点是,filter列表(本例中的keys变量)不能太大,或者您可能达到参数限制,例外情况如下:

SqlException:传入的请求包含太多参数。 服务器最多支持2100个参数。 减少参数数量并重新发送请求。

因此,在此示例中(每行有三个参数),您不能有超过700个要过滤的位置。

使用两个项进行过滤,它将在最终的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中。

最后,这里是Universal PredicateBuilder的代码,用于记录。

 ///  /// Enables the efficient, dynamic composition of query predicates. ///  public static class PredicateBuilder { ///  /// Creates a predicate that evaluates to true. ///  public static Expression> True() { return param => true; } ///  /// Creates a predicate that evaluates to false. ///  public static Expression> False() { return param => false; } ///  /// Creates a predicate expression from the specified lambda expression. ///  public static Expression> Create(Expression> predicate) { return predicate; } ///  /// Combines the first predicate with the second using the logical "and". ///  public static Expression> And(this Expression> first, Expression> second) { return first.Compose(second, Expression.AndAlso); } ///  /// Combines the first predicate with the second using the logical "or". ///  public static Expression> Or(this Expression> first, Expression> second) { return first.Compose(second, Expression.OrElse); } ///  /// Negates the predicate. ///  public static Expression> Not(this Expression> expression) { var negated = Expression.Not(expression.Body); return Expression.Lambda>(negated, expression.Parameters); } ///  /// Combines the first expression with the second using the specified merge function. ///  static Expression Compose(this Expression first, Expression second, Func 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 => ps, p => pf); // 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(merge(first.Body, secondBody), first.Parameters); } class ParameterRebinder : ExpressionVisitor { readonly Dictionary map; ParameterRebinder(Dictionary map) { this.map = map ?? new Dictionary(); } public static Expression ReplaceParameters(Dictionary 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查询,并使用本地列表中的数据注入VALUES构造。 并且在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 

您是否尝试过使用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方法中新建一个对象时,它每次都会创建一个新对象。 由于这些对象是匿名的,因此它们将被比较的方式与它们的参考相对应,这对于每个对象将是不同的。

另外,看看Jacek的答案。

  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的原因

-编辑-

如果你有多个密钥元组,我认为你必须循环遍历它们并对每个元组进行上述查询。 再次,这可能看起来不够优秀,但是在单个查询中重新检索所有位置的查询可能最终会持续很长时间:

 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做到这一点。

我用更宽的IEnumerable的Any扩展方法替换Contains(这是一个特定于列表和数组的方法):

 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;