在 EF Core 中使用表达式树会产生奇怪的 SQL 语句

Posted

技术标签:

【中文标题】在 EF Core 中使用表达式树会产生奇怪的 SQL 语句【英文标题】:Using expression trees with EF Core produces strange SQL statements 【发布时间】:2021-10-19 16:13:08 【问题描述】:

我正在使用表达式树在运行时动态构建我的表达式,然后在IQueryable<T>.Where() 子句中使用它,因为我称之为我的数据库。生成的 SQL 语句对我来说看起来非常奇怪,我不明白那里发生了什么。

一般信息:项目使用 Framework 4.7.2、EF Core 3.1.3 NuGet

示例(以下为简化):

考虑这样一个客户类:

public class Customer
    public string Name get; set;
    public int Age get; set;
    public string Address get; set;

DBContext 用DBSet<Customer> 等正确设置。

所以现在我想使用... db.Customer.Where(expression).ToList() ... 之类的东西,我必须在运行时构建相应的表达式。程序将获得用于类似List<(string, string)> 的搜索条件列表,其中第一个字符串将是客户属性的名称,第二个是用于过滤的值。这种传递搜索条件的方式无法更改。

我像这样构建我的表达式树,已将搜索条件列表转换为字典 (Dictionary<string, List<string>>),其中属性名称作为键和要搜索的实际值的值列表。 typesDictionary 保存有关属性类型的信息(名称 -> 字符串等):

var param = Expression.Parameter(typeof(Customer), "c");
var andList = new List<Expression>();

foreach (var sc in searchCriteria)
    var orList = new List<Expression>();
    foreach (var value in sc.Value
       var expr = Expression.Equal(
       Expression.Property(param, sc.Key),
       Expression.Constant(Convert.ChangeType(value, typesDictionary[sc.Key]), typesDictionary[sc.Key]));
       orList.Add(expr);
     
     andList.Add(orList.Aggregate(Expression.Or));

var expression = Expression.Lambda<Func<Customer, bool>>(andList.Aggregate(Expression.And), param);

例如,结果表达式类似于 c =&gt; ((c.Name == "Bob") OR (c.Name == "John")) AND (c.Age == 12)。有点过度使用括号...

关于 SQL,我希望它是这样的:

SELECT c.Name, c.Age, c.Address
FROM someDB.someSchema.Customers as c
WHERE c.Name = "John" OR c.Name = "Bob" AND c.Age = 13

但是创建的内容是这样的:

SELECT [c].[Name], [c].[Aage], [c].[Address]
      FROM [Customer] AS [c]
      WHERE ((CASE
          WHEN [c].[Age] = CAST(12 AS int) THEN CAST(1 AS bit)
          ELSE CAST(0 AS bit)
      END | CASE
          WHEN [c].[Age] = CAST(15 AS int) THEN CAST(1 AS bit)
          ELSE CAST(0 AS bit)
      END) | CASE
          WHEN [c].[Age] = CAST(22 AS int) THEN CAST(1 AS bit)
          ELSE CAST(0 AS bit)
      END) = CAST(1 AS bit)

这里发生了什么? CASE WHENCASTS 都是从哪里来的?

【问题讨论】:

您可能需要给出确切的示例而不是“类似”,因为括号中的差异可能会影响事物。实际的例子似乎是创建一个真值表,它可能表明整数(12、15 和 22)是 13 岁的 Johns & Bobs 的 ID?更准确地添加更多细节(例如客户的源表)可能会使问题更清晰。 但归根结底,它是否有效/它是否高效/它的执行计划中是否有任何危险信号?我不会真正关心 SQL 的样子,只关心它的执行方式;无论如何,优化器实际上都会重写它 @CaiusJard 好点。我假设WHEN [c].[Age] 调用是混淆而不是查询的整体外观 - 这就是为什么确切的示例会使解决方案更加明显。 是的,它看起来有点奇怪,相当于if((name == "Caius" ? true : false) == true) 的SQL,但它最终可能会被优化为相同的整体! :D 好吧,我的例子很糟糕。说明:重要的部分是表达式创建和生成的 SQL。其他的只是随机的例子,甚至不是来自同一个查询。它们在那里粗略地显示发生了什么以及代码产生了什么。不幸的是,我无法显示生产代码。性能是一个问题,因为搜索条件列表可能有数百个 Property - Value 对。其中 500 个发生异常 SQL statement is to deeply nested。生成的 SQL 脚本的性能比“普通”SQL 差得多。 【参考方案1】:

您的问题是Expression.Or 是bitwise or,所以基本上是| 运算符。而Expression.And 是bitwise and (&amp;)。所以你的表达是:

c => (c.Name == "John" | c.Name == "Bob" ) & c.Age = 13;

你想要的(你会如何手写)是这样的:

c => (c.Name == "John" || c.Name == "Bob" ) && c.Age = 13;

为此,您需要使用Expression.OrElseExpression.AndAlso

foreach (var sc in searchCriteria)
    var orList = new List<Expression>();
    foreach (var value in sc.Value) 
        var expr = Expression.Equal(
            Expression.Property(param, sc.Key),
            Expression.Constant(Convert.ChangeType(value, typesDictionary[sc.Key]), typesDictionary[sc.Key]));
        orList.Add(expr);
    
    andList.Add(orList.Aggregate(Expression.OrElse));

var expression = Expression.Lambda<Func<Customer, bool>>(andList.Aggregate(Expression.AndAlso), param);

之后,您应该会生成更多看起来“正常”的 sql 查询。

【讨论】:

感谢您的提示。明天我会在我回到项目并发布结果时尝试一下。 感谢您的建议。有效。生成的 SQL 语句现在符合预期。我仍然得到Microsoft.Data.SqlClient.SqlException: Some part of your SQL statement is nested too deeply. Rewrite the query or break it up into smaller queries...,因为组合多个表达式树会导致括号数量过多。知道如何解决这个问题 我唯一的想法是收集数组中同一列的所有值并使用包含。因此,不要使用 Name = John 或 Name = Bob 或 ... - 使用 someArray.Contains(Name)。 是的,这就是要走的路。它适用于.Contains(),虽然很难弄清楚如何在运行时创建List&lt;T&gt;,当你只知道运行时T是什么并提取相应的.Contains方法时。

以上是关于在 EF Core 中使用表达式树会产生奇怪的 SQL 语句的主要内容,如果未能解决你的问题,请参考以下文章

我可以重用代码来为 EF Core 的子属性选择自定义 DTO 对象吗?

由于产生时间成本,禁用 EF Core 3.1 的模型验证

尝试使用LINQ to SQL创建节点树会产生NotSupportedException

.Net EF Core千万级数据实践

EF Core 2.0 中的动态访问表

使用 Devart.MySql 将 EF6 升级到 EF Core 3