EF和LINQ中的DefaultIfEmpty混乱订购项对实体?

时间:2018-07-20 14:44:57

标签: entity-framework linq

给出该实体(以及该记录作为示例)

Discount
Amount Percentage
1000          2
5000          4
10000         8

我想获取适用于一个P.O的百分比金额

IE .:有一个P.O.数量15000

如果我使用

db.Discount
  .Where(d => d.Amount <= PO.Amount)
  .OrderByDescending(o => o.Amount)
  .Select(s => s.Percentage)
  .ToList()
  .DefaultIfEmpty(0)
  .FirstOrDefault();

我得到8分(正确)

但是如果我使用

db.Discount
  .Where(d => d.Amount <= PO.Amount)
  .OrderByDescending(o => o.Amount)
  .Select(s => s.Percentage)
  .DefaultIfEmpty(0)
  .FirstOrDefault();

我得到2(不正确),并且不再订购商品。

我对DefaultIfEmpty的使用不正确吗?

2 个答案:

答案 0 :(得分:1)

这是正常现象,因为

第一句话

db.Discount.Where(d => d.Amount <= PO.Amount).OrderByDescending(o => o.Amount).Select(s => s.Percentage).ToList().DefaultIfEmpty(0).FirstOrDefault();

您在 DefaultIfEmpty(0)之前调用 .ToList(),这意味着您在调用.ToList()语句时,将其转换为sql,如下所示

DECLARE @p0 Int = 15000
SELECT [t0].[Percentage]
FROM [AppScreens] AS [t0]
WHERE [t0].[Amount] <= @p0
ORDER BY [t0].[Amount] DESC

然后这两个函数在内存.DefaultIfEmpty(0).FirstOrDefault();中的数据上运行后执行并加载到内存中,因此结果如您所愿

但要发表第二句话

   db.Discount.Where(d => d.Amount <= PO.Amount).OrderByDescending(o => o.Amount).Select(s => s.Percentage).DefaultIfEmpty(0).FirstOrDefault();

您不会调用.ToList(),这意味着直到到达 FirstOrDefault()为止,该语句才会执行,因为 DefaultIfEmpty(0)函数是由使用延迟执行,您可以从MSDN

的引用中阅读其文档

达到 .FirstOrDefault()语句后,将其转换为sql如下

DECLARE @p0 Int = 15000
SELECT  case when [t2].[test] = 1 then [t2].[Percentage] else [t0].[EMPTY] end AS [value]
FROM (SELECT 0 AS [EMPTY] ) AS [t0]
LEFT OUTER JOIN ( SELECT TOP (1) 1 AS [test], [t1].[Percentage] FROM [Discount] AS [t1] WHERE [t1].[Amount] <= @p0 ) AS [t2] ON 1=1 
ORDER BY [t2].[Amount] DESC

然后执行并加载到内存中,因此结果与预期不同 因为它先获得排名第一,然后再获得订单,因此它获得了第一项。

答案 1 :(得分:0)

如果您使用的是Entity Framework 6. *,则它是known bug

  

解决方法是将DefaultIfEmpty调用移到ToList之后,这可以说是更好的选择,因为不需要在数据库中替换空结果集。

以下是使用EF 6.1.2生成的示例(并在Microsoft SQL Server 2016上使用Microsoft SQL Profiler“捕获”)。

现在...您的“错误”查询:

var res = db.Discounts
    .Where(d => d.Amount <= PO.Amount)
    .OrderByDescending(o => o.Amount)
    .Select(s => s.Percentage)
    .DefaultIfEmpty(0)
    .FirstOrDefault();

“删除” OrderBy

exec sp_executesql N'SELECT 
    [Limit1].[C1] AS [C1]
    FROM ( SELECT TOP (1) 
        CASE WHEN ([Project1].[C1] IS NULL) THEN cast(0 as bigint) ELSE [Project1].[Percentage] END AS [C1]
        FROM   ( SELECT 1 AS X ) AS [SingleRowTable1]
        LEFT OUTER JOIN  (SELECT 
            [Extent1].[Percentage] AS [Percentage], 
            cast(1 as tinyint) AS [C1]
            FROM [dbo].[Discount] AS [Extent1]
            WHERE [Extent1].[Amount] <= @p__linq__0 ) AS [Project1] ON 1 = 1
    )  AS [Limit1]',N'@p__linq__0 bigint',@p__linq__0=15000

“最佳”查询为:

var res = db.Discounts
    .Where(d => d.Amount <= PO.Amount)
    .OrderByDescending(o => o.Amount)
    .Select(s => s.Percentage)
    .Take(1)
    .ToArray()
    .DefaultIfEmpty(0)
    .First(); // Or Single(), same result but clearer that there is always *one* element

看到Take(1)吗?它会生成一个TOP (1)

exec sp_executesql N'SELECT TOP (1) 
    [Project1].[Percentage] AS [Percentage]
    FROM ( SELECT 
        [Extent1].[Amount] AS [Amount], 
        [Extent1].[Percentage] AS [Percentage]
        FROM [dbo].[Discount] AS [Extent1]
        WHERE [Extent1].[Amount] <= @p__linq__0
    )  AS [Project1]
    ORDER BY [Project1].[Amount] DESC',N'@p__linq__0 bigint',@p__linq__0=15000

然后ToArray()会将详细说明移至C#。您可以将.FirstOrDefault()??一起使用,而不要使用DefaultIfEmpty(),但是如果Amount已经可以为空({{1返回的null }}是因为没有行,或者因为找到的唯一行有FirstOrDefault()?谁知道:-))。要解决此问题,它会变得更加复杂(在大多数情况下):

Amount == null

此处var res = (db.Discounts .Where(d => d.Amount <= PO.Amount) .OrderByDescending(o => o.Amount) .Select(s => new { s.Percentage }) .FirstOrDefault() ?? new { Percentage = (long)0 } ).Percentage; 中的(long)应该是(long)0的数据类型。该查询给出:

Percentage

其他“更差”的变体:

exec sp_executesql N'SELECT TOP (1) 
    [Project1].[C1] AS [C1], 
    [Project1].[Percentage] AS [Percentage]
    FROM ( SELECT 
        [Extent1].[Amount] AS [Amount], 
        [Extent1].[Percentage] AS [Percentage], 
        1 AS [C1]
        FROM [dbo].[Discount] AS [Extent1]
        WHERE [Extent1].[Amount] <= @p__linq__0
    )  AS [Project1]
    ORDER BY [Project1].[Amount] DESC',N'@p__linq__0 bigint',@p__linq__0=15000

给出带有两个var res = db.Discounts .Where(d => d.Amount <= PO.Amount) .OrderByDescending(o => o.Amount) .Select(s => s.Percentage) .Take(1) .DefaultIfEmpty(0) .First(); 的过于复杂的查询:

TOP (1)