【问题标题】:LINQ to Entities - where..in clause with multiple columnsLINQ to Entities - 具有多列的 where..in 子句
【发布时间】:2011-10-18 06:29:06
【问题描述】:

我正在尝试使用 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 中是否有一种“好”的方式来表达这一点,同时能够在数据库上运行查询?或者,如果我只是迭代键并将查询合并在一起,那会不会对性能不利?

【问题讨论】:

    标签: c# linq-to-entities multiple-columns where-in


    【解决方案1】:

    怎么样:

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

    更新

    不幸的是,EF 会在此引发 NotSupportedException,如果您需要在数据库端运行查询,则会取消此答案的资格。

    更新 2

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

    【讨论】:

    • 因为问题是针对 Linq to Entities 我怀疑这会起作用,否则是个好建议。
    • 我现在正在测试它,看看 EF 是否理解这一点。我使用的另一个 ORM 也可以。
    • 我会接受这个作为详细的“这在 LINQ-to-EF 中似乎不可能”的答案。在我的情况下,数据量并不高,所以我与Union()一起使用查询(因为在 LINQ 中动态构建谓词很痛苦),并且交叉手指说 SQL Server 可以找出所有的命中都是相同的索引。
    【解决方案2】:

    虽然我无法让@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 个位置。

    使用两项过滤,在最终的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'
    

    注意最初的“假”表达式是如何被 EntityFramework 正确忽略的,并且没有包含在最终的 SQL 中。

    最后,这里是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);
            }
        }
    }
    

    【讨论】:

      【解决方案3】:

      我的解决方案是构建一个新的扩展方法 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
                                                )
                                );
      

      【讨论】:

      • 有趣的方法,不知道可以用LinqKit实现吗?
      • 我尝试使用您的扩展,但 ICle 未定义。你能包括ICle的定义吗?
      【解决方案4】:
      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;
      

      【讨论】:

      • 这是我创建简化示例时的错字,我解决了这个问题。
      • 我很喜欢这个答案,唯一缺少的是选择行和查询的结尾。
      【解决方案5】:

      存在一个 EF 扩展,它被设计为非常相似的情况。它是 EntityFrameworkCore.MemoryJoin(名称可能令人困惑,但它同时支持 EF6 和 EF Core)。正如作者article 中所述,它修改了传递给服务器的 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
      

      【讨论】:

      • 这看起来很有希望!不幸的是,我实际上无法验证它是否能完成这项工作,因为我现在在一个完全不同的项目上,但当问题再次发生时,我会记住它。
      【解决方案6】:

      您是否尝试过仅使用 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))
      

      【讨论】:

      • 这不能编译:Delegate 'System.Func' does not take 1 arguments
      【解决方案7】:

      如果您不需要很多组合键,您可以简单地将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 属性。

      不理想,但应该可以。

      【讨论】:

      • 我正在使用的数据库架构实际上将位置组件映射到数据库中的一个键,而我正在构建的查询正在查找这些。不过,将它们散列在一起而不是存储显式映射的想法是一个很好的想法。
      【解决方案8】:

      我认为这对你不起作用,因为当你在 Contains 方法中更新一个对象时,它每次都会创建一个新对象。由于这些对象是匿名的,因此它们的比较方式是针对它们的引用,这对于每个对象都是不同的。

      另外,看看 Jacek 的回答。

      【讨论】:

      • 这里有一个问题。根据msdn.microsoft.com/en-us/library/bb397696.aspx 只有当它们的所有属性都相等时,相同匿名类型的两个实例才相等。这意味着克里斯的方式也应该有效。
      • @Thomas: Contains 使用相等比较器,匿名类型使用属性相等 - 这不是问题。
      【解决方案9】:
          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
      

      试试这个。

      【讨论】:

      • 我相信这与@Jacek 的答案相同,它在 LINQ-to-EF 中不起作用。
      【解决方案10】:

      我认为正确的做法是

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

      【讨论】:

      • 是的,生成完整查询而不是使用or 方法会更长,但可以将短查询变成准备好的语句,从而更快。我不确定 EF 是否支持其中任何一项
      【解决方案11】:

      您可以在投影上投影一个字符串连接键并匹配。但是,请注意,您将无法使用在列上构建的任何索引,并且将进行字符串匹配,这可能会很慢。

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

      【讨论】:

        【解决方案12】:

        如何使用基于多列的 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])))
        

        【讨论】:

          【解决方案13】:

          我会用更广泛的 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;
          

          【讨论】:

          • 我相信已经有几个人给出了这个答案,这在 LINQ-to-EF 中不起作用。
          猜你喜欢
          • 2010-10-25
          • 2023-01-13
          • 1970-01-01
          • 2010-09-30
          • 1970-01-01
          • 2012-02-25
          • 2018-04-22
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多