进行这样一个查询,统计用户积分(point是int类型字段),代码很简单,结果却报错了:
1 await DbContext.Set<GetPointDetail>().Where(p => p.OwnerId == User.Id).SumAsync(p => p.Point);
日志显示,查询结果为null,转换为int类型失败,必须使用可空类型。好吧,看到结果为null我就知道问题出在哪了。
先来看看该行代码生成的sql语句:
1 exec sp_executesql N'SELECT 2 [GroupBy1].[A1] AS [C1] 3 FROM ( SELECT 4 SUM([Extent1].[Point]) AS [A1] 5 FROM [dbo].[GetPointDetails] AS [Extent1] 6 WHERE [Extent1].[OwnerId] = @p__linq__0 7 ) AS [GroupBy1]',N'@p__linq__0 varchar(8000)',@p__linq__0='xxx' 8 go
熟悉sql server的应该知道,sum函数是会过忽略NULL值而非作为0参与计算,而如果查询结果没有一条数据,会直接返回NULL,我遇到的正是这种情况。而代码中的IQueryable.SumAsync()返回值是int,就是说,内部会将数据库返回的NULL值转换为int,显然就会报错。
那么如何修改呢?先看第一种方案,使用DefaultIfEmpty方法:
1 await DbContext.Set<GetPointDetail>().Where(p => p.OwnerId == User.Id).Select(p => p.Point).DefaultIfEmpty().SumAsync();
1 exec sp_executesql N'SELECT 2 [GroupBy1].[A1] AS [C1] 3 FROM ( SELECT 4 SUM([Join1].[A1]) AS [A1] 5 FROM ( SELECT 6 CASE WHEN ([Project1].[C1] IS NULL) THEN 0 ELSE [Project1].[Point] END AS [A1] 7 FROM ( SELECT 1 AS X ) AS [SingleRowTable1] 8 LEFT OUTER JOIN (SELECT 9 [Extent1].[Point] AS [Point], 10 cast(1 as tinyint) AS [C1] 11 FROM [dbo].[GetPointDetails] AS [Extent1] 12 WHERE [Extent1].[OwnerId] = @p__linq__0 ) AS [Project1] ON 1 = 1 13 ) AS [Join1] 14 ) AS [GroupBy1]',N'@p__linq__0 varchar(8000)',@p__linq__0='xxx' 15 go
DefaultIfEmpty()会对集合元素做空判断,为null就返回默认值,生成的sql表现为case when判断,不过这种sql明显不是一个好sql。
那么从日志的错误信息入手,日志已经告诉我们,“must use a nullable type”,要使用int的可空类型,所以第二种方案就是对point字段做个“int?”显示转换:
1 await DbContext.Set<GetPointDetail>().Where(p => p.OwnerId == User.Id).SumAsync(p => (int?)p.Point);
生成的sql同最初一样。这样无论结果是整数还是NULL,都能正确转换。
提一句,可空值类型也是值类型,两者转换不会进行装箱/拆箱。