给出该实体(以及该记录作为示例)
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的使用不正确吗?
答案 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)