c# - EF LINQ to SQL,除以零错误,生成的查询将参数置于错误的顺序

标签 c# sql-server entity-framework linq

编辑在帖子底部重现此错误的步骤

此问题的我的数据结构:

    public class StockRequest
    {
        public int StartYear { get; set; }
        public StockInterval StockInterval { get; set; }
    }

    public class StockInterval
    {
        /// <summary>
        ///  Can be 0 = non-recurring, 1 = annual, 2 = once every 2 years, 3 = once every 3 years
        /// </summary>
        public int IntervalInYears { get; set; }
    }

如果我想获取 2021 年的所有库存请求。以下数据将满足该条件:

var nonRecurringRequest = new StockRequest() { StartYear = 2021, StockInterval = new StockInterval() { IntervalInYears = 0 } };
var annualRequest = new StockRequest() { StartYear = 2020, StockInterval = new StockInterval() { IntervalInYears = 1 } };
var everyTwoYearsRequest = new StockRequest() { StartYear = 2019, StockInterval = new StockInterval() { IntervalInYears = 2 } };
var everyThreeYearsRequest = new StockRequest() { StartYear = 2018, StockInterval = new StockInterval() { IntervalInYears = 3 } };

EF查询中关键的where子句是:

query.Where(x => 
   x.StartYear <= selectedYear && 
  (
    x.StartYear == selectedYear || 
    (x.StockInterval.IntervalInYears != 0 && selectedYear - x.StartYear % x.StockInterval.IntervalInYears == 0) 
  )
);

导致问题的部分是非经常性库存请求(间隔为 0)。你不能修改它,因为这样你就除以零。但是,我知道这一点,并且过去通过在尝试修改之前首先检查属性 (IntervalInYears) 是否不为零来解决此问题。由于 WHERE 的第一部分检查失败,因此不会继续到 mod 部分。

由于某种原因,这次不起作用。当我检查生成的查询时,它会将 0 放在前面:

WHERE 
StockRequests.[StartYear] <= @stockYear
AND 
(
    StockRequests.[StartYear] = @stockYear OR 
    (
        0 <> StockIntervals.[IntervalInYears] AND 
        0 = (@stockYear - StockRequests.[StartYear]) % StockIntervals.[IntervalInYears] 
    )
)

在 SQL Server 中执行该操作会生成被零除错误。但是,翻转 0 和 StockIntervals.IntervalInYears 的两边:

WHERE 
StockRequests.[StartYear] <= @stockYear
AND 
(
    StockRequests.[StartYear] = @stockYear OR 
    (
        StockIntervals.[IntervalInYears] <> 0  AND 
        0 = (@stockYear - StockRequests.[StartYear]) % StockIntervals.[IntervalInYears] 
    )
)

现在可以正常使用了。为什么 EF 要改变这个问题以及如何在 EF 中修复它?我没有在 EF 查询中将 0 放在第一位,而且我不记得以前发生过这种情况,这是我一直使用的解决方案,以确保我没有尝试除以零并且它曾经有效。我知道我可以手动编写 SQL 查询并执行它,但投影超过 200 行。

编辑 重现: 表创建脚本:

CREATE TABLE [dbo].[StockIntervals](
[Id] [uniqueidentifier] NOT NULL,
[Name] [nvarchar](255) NOT NULL,
[IntervalInYears] [int] NOT NULL,
 CONSTRAINT [PK_dbo.StockIntervals] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[StockIntervals] ADD  DEFAULT ((0)) FOR [IntervalInYears]
GO

CREATE TABLE [dbo].[StockRequests](
[Id] [uniqueidentifier] NOT NULL,
[Count] [int] NOT NULL,
[DateRequested] [datetime] NOT NULL,
[StartYear] [int] NOT NULL,
[StockIntervalId] [uniqueidentifier] NOT NULL,
[EndYear] [int] NULL,


CONSTRAINT [PK_dbo.StockRequests] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[StockRequests]  WITH CHECK ADD  CONSTRAINT [FK_dbo.StockRequests_dbo.StockIntervals_StockIntervalId] FOREIGN KEY([StockIntervalId])
REFERENCES [dbo].[StockIntervals] ([Id])
GO

ALTER TABLE [dbo].[StockRequests] CHECK CONSTRAINT [FK_dbo.StockRequests_dbo.StockIntervals_StockIntervalId]
GO

填充表:

INSERT INTO [dbo].[StockIntervals]
       ([Id]
       ,[Name]
       ,[IntervalInYears])
 VALUES
       ('738A431E-D517-4C17-9ECA-A1A0942E236B', 'Non-recurring one time', 0),
       ('CCB746A7-F644-4C7E-ADBE-AE14DE01B19E', 'Annual', 1),
       ('80C6CAE6-5287-41E6-A5FE-AAA53035EC19', 'Every 2 years', 2),
       ('B34EE256-C40B-4F03-8232-14B681186C7A', 'Every 3 years', 3)

GO

INSERT INTO [dbo].[StockRequests]
       ([Id]
       ,[Count]
       ,[DateRequested]
       ,[StartYear]
       ,[StockIntervalId]
       ,[EndYear])
 VALUES
       ('4a5ae94e-0a85-4195-8e7e-8cc556307b30'
       ,15
       ,'2022-01-11 15:16:41.567'
       ,2021
       ,'738A431E-D517-4C17-9ECA-A1A0942E236B'
       ,null),
       ('f0d83b68-0da1-4824-9eeb-2e52ff369db5'
       ,60
       ,'2022-01-11 15:16:41.567'
       ,2020
       ,'CCB746A7-F644-4C7E-ADBE-AE14DE01B19E'
       ,null),
       ('a49b4b9e-80d6-4fca-ad78-6c8996616c97'
       ,1000
       ,'2022-01-11 15:16:41.567'
       ,2019
       ,'80C6CAE6-5287-41E6-A5FE-AAA53035EC19'
       ,null),
       ('cc21a265-f8df-4d2d-9eae-5f6f97ef9909'
       ,50
       ,'2022-01-11 15:16:41.567'
       ,2018
       ,'B34EE256-C40B-4F03-8232-14B681186C7A'
       ,null)
GO

运行此查询:

DECLARE @stockYear int = 2021

SELECT * FROM 
dbo.StockRequests
INNER JOIN dbo.StockIntervals on StockIntervalId = StockIntervals.Id
WHERE 
    StockRequests.[StartYear] <= @stockYear
    AND 
    (
        StockRequests.[StartYear] = @stockYear OR 
        (
            0 <> StockIntervals.[IntervalInYears]  AND 
            0 = (@stockYear - StockRequests.[StartYear]) % StockIntervals.[IntervalInYears] 
        )
    )

没有错误。好的,现在尝试插入一条新记录:

INSERT INTO [dbo].[StockRequests]
VALUES ('FFA820F1-E361-4AC5-AB00-E621BFFEF9B5', 20, '2022-01-11 16:22:11.567', 2020, '738A431E-D517-4C17-9ECA-A1A0942E236B', null)

再次运行查询。发生除零错误。在研究了数据之后,这种行为是有道理的。如果 @stockYear 大于或小于 StartYear 且该记录的间隔为零,则会出错,因为如果到达查询的最内部部分,且间隔为零且它没有 bool 表达式快捷方式。好的。

但是将查询的一行切换为:

StockIntervals.[IntervalInYears] <> 0

现在可以了!不知道这是如何巧合的,我已经在很多场景中运行我的脚本来触发错误,但它总是通过上述解决。如果没有短路,切换操作数仍会导致错误。但事实并非如此。所以人们说操作数顺序并不重要,但我能够证明它看起来很重要。

最佳答案

您似乎假设 T-SQL 中的 ANDOR 始终按照查询中指定的顺序短路。事实并非如此。

确实,它通常会短路逻辑表达式。毕竟,为什么要做不必要的工作呢?但它可能不符合查询中指定的顺序。逻辑运算符没有被指定以任何特定顺序执行,优化器通常会根据短路可能性的估计或评估中涉及的工作量等因素来选择切换它们,只要遵循运算符优先级规则(AND 位于 OR 之前,等等)。

由于评估所有可能的执行计划的空间太大,优化器使用积极的修剪来删除基于启发式的选项。这两个谓词:

(
    StockRequests.[StartYear] = @stockYear OR 
    (
        0 <> StockIntervals.[IntervalInYears]  AND 
        0 = (@stockYear - StockRequests.[StartYear]) % StockIntervals.[IntervalInYears] 
    )
)

(
    StockRequests.[StartYear] = @stockYear OR 
    (
        StockIntervals.[IntervalInYears] <> 0  AND 
        0 = (@stockYear - StockRequests.[StartYear]) % StockIntervals.[IntervalInYears] 
    )
)

就查询意图而言完全相同。问题是优化器将选择如何处理它们。在您的情况下,以一种方式放置比较器会导致某些优化到位(或不到位),因此 AND 可能会翻转。

正如您从 this fiddle 中看到的那样,在 SQL Server 2019 上运行,两个选项都正确短路,翻转 AND 也是如此。我必须翻转 OR 才能使其失败,然后 AND 的顺序并不重要。请注意,任何查询中的逻辑均未更改,并且 AND = 比较器本身的顺序不会强制手对于优化器来说,它有时只是引导它沿着特定的路径前进。

因此,这很大程度上取决于优化器决定做什么,并且您无法预先保证它始终会正确执行。是的,您看到它这样做了一百次,但是第一百零一次可能会发生变化,可能是因为统计数据更改,或者 SQL Server 更新,或者更改基数估计器版本,或者数据库兼容性级别,或者任何许多事情都可能导致重新编译。

确保按特定顺序短路的唯一保证方法是使用CASE(或NULLIF 编译成CASE)。 This is documented by Microsoft ,只要您不使用任何聚合函数,它就可以工作。

In other words, do not expect something like CASE WHEN x > 0 THEN SUM(1 / x) END to work, because the SUM is often evaluated at an earlier stage. It only works with scalar values. As far as I am aware I would expect the same issue would apply to subqueries and window functions.

因此,您可以使用 NULLIF 解决您的问题

(
    StockRequests.[StartYear] = @stockYear OR 
    (
        StockIntervals.[IntervalInYears] <> 0  AND 
        0 = (@stockYear - StockRequests.[StartYear]) % NULLIF(StockIntervals.[IntervalInYears], 0)
    )
)

在 Entity Framework 中,您可以使用类似 (value == 0 ? null : value)

query.Where(x => 
   x.StartYear <= selectedYear && 
  (
    x.StartYear == selectedYear || 
    (x.StockInterval.IntervalInYears != 0
     && selectedYear - x.StartYear %
        (x.StockInterval.IntervalInYears == 0 ? null : x.StockInterval.IntervalInYears)
        == 0) 
  )
);

关于c# - EF LINQ to SQL,除以零错误,生成的查询将参数置于错误的顺序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70672585/

相关文章:

sql - 在SQL Server中,如何在存储过程中传递可选参数?

c# - 如何检索 Entity Framework 中的特定列

entity-framework - 迁移导致 "Sequence contains no elements"错误

c# - 通用对象池

c# - 如何正确重试 HttpRequestMessage?

c# - VB.net 到 C# 等效于 "AddressOf"

c# - 访问/管理多组织的数据库设计

c# - 序列化使用 Json.Net 声明为新的继承属性不起作用

sql - 用于测试非空字符串和非空字符串的兼容 SQL

mysql - 如何使用 Entity Framework 6 MySQL 更新记录