使用 EF Core Linq2Sql 聚合聚合
Aggregate of aggregate with EF Core Linq2Sql
我有一个带有 EF Core 2.2 代码优先数据库的 ASP.NET Core 2.2 项目。我有以下实体:
- 建筑物,基本上是一个地址和一些其他重要数据。
- 楼层,包含楼层数。建筑物可以有多个楼层。一个楼层必须正好有一个建筑物所在的位置。
- 房间,有编号。一个楼层可以有多个房间。一个房间必须只有一层。
- WorkGroup,包含有多少员工在群里,群是否还活跃,群是什么时候开始运作的(可以是将来)。
- RoomOccupancy,它是工作组和房间之间的连接 table,显示工作组 is/was/will 在哪个房间。
我需要一份建筑物列表,其中包含建筑物名称、它有多少层、建筑物有多少个房间(不是一层)以及目前有多少人在建筑物内工作。
目前我能够获取所有数据,但翻译后的 SQL 不是最佳的并且需要多次访问数据库。我能够手写一个 SQL select 语句(内部 select)来解决这个问题,所以我知道这应该可以通过一个查询实现。
dbContext.Buildings.Select(x=> new BuildingDatableElementDTO(){
BuildingId = b.Id,
Name = b.Name,
FloorCount = b.Floors.Count(),
//this is the part where problems start,
//this translates to multiple SQL statements
RoomCount = b.Floors.Sum(f=>f.Rooms.Count()),
// I replaced the next line with
// CurrentWorkerCount = 10, but a solution would be nice
CurrentWorkerCount = b.Floors.Sum(f=>f.Rooms
.Sum(r=>r.RoomOccupancies
.Where(o=>!o.WorkGroup.IsFinished && o.WorkGroup.StartDate < Datetime.Now).
.Sum(w => w.NumberOfEmployees)
))),
}).ToList();
出于测试目的,我将 CurrentWorkerCount lambda 替换为 CurrentWorkerCount = 10,因为我可以理解它是否很难转换为 SQL,但它仍然无法创建一个 SQL 语句房间数。
带有信息级别的日志记录显示:"The LINQ expression '"Sum()“'无法翻译,将在本地评估”对于至少有一层的建筑物。
然后我有一个更大的 DbCommand(太长无法复制),然后每个建筑物都有一个 DbCommand,它计算房间的数量。
我了解到 EF Core 2.1 的聚合存在问题,但我认为 ORM 将此投影转换为一个查询应该不是一项艰巨的任务。
我是不是哪里做错了,或者这些是 LINQ 和 EF Core 的功能?我想我以前可以使用非核心 EF 轻松做到这一点。我阅读了有关 GroupBy 和聚合的一些解决方法,但对我的情况没有帮助。
更新
这是生成的日志(只有有趣的部分)。我正在使用自定义解决方案来过滤、排序和分页,它非常适合解决简单问题。此示例中没有过滤,按建筑物名称和基本提取排序(跳过 0 取 15)。数据库中只有极少量的测试数据(15 栋楼,一个有 1 层,另一个有 2 层,其中一个有 1 个房间,其中有 1 个工作组和 100 名员工)。我还使用为 IsDeleted 标志配置的全局过滤器进行软删除。我认为这些因素不会影响结果,但它们在这里,也许会。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 已执行 DbCommand ("2"ms) [Parameters=["@__p_0='?' (DbType = Int32), @__p_1='?' (DbType = Int32)"], CommandType ='Text', CommandTimeout='30']"
SELECT CONVERT(VARCHAR(36), [x].[Id]) AS [BuildingId], [x].[Name], (
SELECT COUNT(*)
FROM [Floors] AS [x0]
WHERE ([x0].[IsDeleted] = 0) AND ([x].[Id] = [x0].[BuildingId])
) AS [FloorCount], [x].[Id]
FROM [Buildings] AS [x]
WHERE [x].[IsDeleted] = 0
ORDER BY [x].[Name]
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@__Now_2='?' (DbType = DateTime2), @_outer_Id3='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT SUM([x14].[NumberOfEmployees])
FROM [RoomOccupancys] AS [x14]
LEFT JOIN [WorkGroups] AS [k.WorkGroup2] ON [x14].[WorkGroupId] = [k.WorkGroup2].[Id]
WHERE (([x14].[IsDeleted] = 0) AND (([k.WorkGroup2].[IsFinished] = 0) AND ([k.WorkGroup2].[StartDate] < @__Now_2))) AND ([x13].[Id] = [x14].[RoomId])
)
FROM [Rooms] AS [x13]
WHERE ([x13].[IsDeleted] = 0) AND (@_outer_Id3 = [x13].[FloorId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@__Now_2='?' (DbType = DateTime2), @_outer_Id3='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT SUM([x14].[RemainingAmount])
FROM [RoomOccupancys] AS [x14]
LEFT JOIN [WorkGroups] AS [k.WorkGroup2] ON [x14].[WorkGroupId] = [k.WorkGroup2].[Id]
WHERE (([x14].[IsDeleted] = 0) AND (([k.WorkGroup2].[IsFinished] = 0) AND ([k.WorkGroup2].[StartDate] < @__Now_2))) AND ([x13].[Id] = [x14].[RoomId])
)
FROM [Rooms] AS [x13]
WHERE ([x13].[IsDeleted] = 0) AND (@_outer_Id3 = [x13].[FloorId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
如果您不使用 EF Navigations 属性,而是通过 LINQ to EF 手动连接会怎样?
var ans2 = (from b in dbContext.Buildings
join f in dbContext.Floors on b.Id equals f.BuildingId into fj
from f in fj.DefaultIfEmpty()
join r in dbContext.Rooms on f.Id equals r.FloorId into rj
from r in rj.DefaultIfEmpty()
join ro in dbContext.RoomOccupancies on r.Id equals ro.RoomId
join w in dbContext.WorkGroups on ro.WorkGroupId equals w.Id into wj
from w in wj.DefaultIfEmpty()
where !w.IsFinished && w.StartDate < DateTime.Now
select new BuildingDatableElementDTO() {
BuildingId = b.Id,
Name = b.Name,
FloorCount = fj.Count(),
RoomCount = rj.Count(),
CurrentWorkerCount = wj.Sum(w => w.NumberOfEmployees)
})
.ToList();
I read that there are problems with the aggregates with EF Core 2.1, but I think it shouldn't be a hard task for the ORM to translate this Projection into one query.
你是对的,EF Core 在翻译 GroupBy
和聚合(而且不仅如此)时存在(并且仍然存在 - 目前最新的 v2.2)问题。但不适用于 "shouldn't be a hard task" - 自己尝试将任意表达式树转换为伪 SQL,您很快就会发现这是一项相当复杂的任务。
无论如何,EF Core 查询翻译随着时间的推移而改进,但如前所述,它远非完美。在这种情况下,阻碍因素是嵌套聚合 - sum/count 等的总和。解决方案是展平目标集并应用单个聚合。例如,按如下方式重写 LINQ 查询:
dbContext.Buildings.Select(b => new //BuildingDatableElementDTO()
{
BuildingId = b.Id,
Name = b.Name,
FloorCount = b.Floors.Count(),
// (1)
RoomCount = b.Floors.SelectMany(f => f.Rooms).Count(),
// (2)
CurrentWorkerCount = b.Floors
.SelectMany(f => f.Rooms)
.SelectMany(r => r.RoomOccupancies)
.Select(o => o.WorkGroup)
.Where(w => !w.IsFinished && w.StartDate < DateTime.Now)
.Sum(w => w.NumberOfEmployees),
})
.ToList();
被翻译成单个 SQL(如预期的那样):
SELECT [e].[Id] AS [BuildingId], [e].[Name], (
SELECT COUNT(*)
FROM [Floors] AS [e0]
WHERE ([e0].[IsDeleted] = 0) AND ([e].[Id] = [e0].[BuildingId])
) AS [FloorCount], (
SELECT COUNT(*)
FROM [Floors] AS [e1]
INNER JOIN (
SELECT [e2].[Id], [e2].[FloorId], [e2].[IsDeleted], [e2].[Name]
FROM [Rooms] AS [e2]
WHERE [e2].[IsDeleted] = 0
) AS [t] ON [e1].[Id] = [t].[FloorId]
WHERE ([e1].[IsDeleted] = 0) AND ([e].[Id] = [e1].[BuildingId])
) AS [RoomCount], (
SELECT SUM([f.Rooms.RoomOccupancies.WorkGroup].[NumberOfEmployees])
FROM [Floors] AS [e3]
INNER JOIN (
SELECT [e4].*
FROM [Rooms] AS [e4]
WHERE [e4].[IsDeleted] = 0
) AS [t0] ON [e3].[Id] = [t0].[FloorId]
INNER JOIN (
SELECT [e5].*
FROM [RoomOccupancies] AS [e5]
WHERE [e5].[IsDeleted] = 0
) AS [t1] ON [t0].[Id] = [t1].[RoomId]
INNER JOIN [WorkGroups] AS [f.Rooms.RoomOccupancies.WorkGroup] ON [t1].[WorkgroupId] = [f.Rooms.RoomOccupancies.WorkGroup].[Id]
WHERE (([e3].[IsDeleted] = 0) AND (([f.Rooms.RoomOccupancies.WorkGroup].[IsFinished] = 0) AND ([f.Rooms.RoomOccupancies.WorkGroup].[StartDate] < GETDATE()))) AND ([e].[Id] = [e3].[BuildingId])
) AS [CurrentWorkerCount]
FROM [Building] AS [e]
WHERE [e].[IsDeleted] = 0
我有一个带有 EF Core 2.2 代码优先数据库的 ASP.NET Core 2.2 项目。我有以下实体:
- 建筑物,基本上是一个地址和一些其他重要数据。
- 楼层,包含楼层数。建筑物可以有多个楼层。一个楼层必须正好有一个建筑物所在的位置。
- 房间,有编号。一个楼层可以有多个房间。一个房间必须只有一层。
- WorkGroup,包含有多少员工在群里,群是否还活跃,群是什么时候开始运作的(可以是将来)。
- RoomOccupancy,它是工作组和房间之间的连接 table,显示工作组 is/was/will 在哪个房间。
我需要一份建筑物列表,其中包含建筑物名称、它有多少层、建筑物有多少个房间(不是一层)以及目前有多少人在建筑物内工作。
目前我能够获取所有数据,但翻译后的 SQL 不是最佳的并且需要多次访问数据库。我能够手写一个 SQL select 语句(内部 select)来解决这个问题,所以我知道这应该可以通过一个查询实现。
dbContext.Buildings.Select(x=> new BuildingDatableElementDTO(){
BuildingId = b.Id,
Name = b.Name,
FloorCount = b.Floors.Count(),
//this is the part where problems start,
//this translates to multiple SQL statements
RoomCount = b.Floors.Sum(f=>f.Rooms.Count()),
// I replaced the next line with
// CurrentWorkerCount = 10, but a solution would be nice
CurrentWorkerCount = b.Floors.Sum(f=>f.Rooms
.Sum(r=>r.RoomOccupancies
.Where(o=>!o.WorkGroup.IsFinished && o.WorkGroup.StartDate < Datetime.Now).
.Sum(w => w.NumberOfEmployees)
))),
}).ToList();
出于测试目的,我将 CurrentWorkerCount lambda 替换为 CurrentWorkerCount = 10,因为我可以理解它是否很难转换为 SQL,但它仍然无法创建一个 SQL 语句房间数。
带有信息级别的日志记录显示:"The LINQ expression '"Sum()“'无法翻译,将在本地评估”对于至少有一层的建筑物。 然后我有一个更大的 DbCommand(太长无法复制),然后每个建筑物都有一个 DbCommand,它计算房间的数量。
我了解到 EF Core 2.1 的聚合存在问题,但我认为 ORM 将此投影转换为一个查询应该不是一项艰巨的任务。
我是不是哪里做错了,或者这些是 LINQ 和 EF Core 的功能?我想我以前可以使用非核心 EF 轻松做到这一点。我阅读了有关 GroupBy 和聚合的一些解决方法,但对我的情况没有帮助。
更新
这是生成的日志(只有有趣的部分)。我正在使用自定义解决方案来过滤、排序和分页,它非常适合解决简单问题。此示例中没有过滤,按建筑物名称和基本提取排序(跳过 0 取 15)。数据库中只有极少量的测试数据(15 栋楼,一个有 1 层,另一个有 2 层,其中一个有 1 个房间,其中有 1 个工作组和 100 名员工)。我还使用为 IsDeleted 标志配置的全局过滤器进行软删除。我认为这些因素不会影响结果,但它们在这里,也许会。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 无法翻译 LINQ 表达式“"Sum()"”,将在本地求值。
- 已执行 DbCommand ("2"ms) [Parameters=["@__p_0='?' (DbType = Int32), @__p_1='?' (DbType = Int32)"], CommandType ='Text', CommandTimeout='30']"
SELECT CONVERT(VARCHAR(36), [x].[Id]) AS [BuildingId], [x].[Name], (
SELECT COUNT(*)
FROM [Floors] AS [x0]
WHERE ([x0].[IsDeleted] = 0) AND ([x].[Id] = [x0].[BuildingId])
) AS [FloorCount], [x].[Id]
FROM [Buildings] AS [x]
WHERE [x].[IsDeleted] = 0
ORDER BY [x].[Name]
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@__Now_2='?' (DbType = DateTime2), @_outer_Id3='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT SUM([x14].[NumberOfEmployees])
FROM [RoomOccupancys] AS [x14]
LEFT JOIN [WorkGroups] AS [k.WorkGroup2] ON [x14].[WorkGroupId] = [k.WorkGroup2].[Id]
WHERE (([x14].[IsDeleted] = 0) AND (([k.WorkGroup2].[IsFinished] = 0) AND ([k.WorkGroup2].[StartDate] < @__Now_2))) AND ([x13].[Id] = [x14].[RoomId])
)
FROM [Rooms] AS [x13]
WHERE ([x13].[IsDeleted] = 0) AND (@_outer_Id3 = [x13].[FloorId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@__Now_2='?' (DbType = DateTime2), @_outer_Id3='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT SUM([x14].[RemainingAmount])
FROM [RoomOccupancys] AS [x14]
LEFT JOIN [WorkGroups] AS [k.WorkGroup2] ON [x14].[WorkGroupId] = [k.WorkGroup2].[Id]
WHERE (([x14].[IsDeleted] = 0) AND (([k.WorkGroup2].[IsFinished] = 0) AND ([k.WorkGroup2].[StartDate] < @__Now_2))) AND ([x13].[Id] = [x14].[RoomId])
)
FROM [Rooms] AS [x13]
WHERE ([x13].[IsDeleted] = 0) AND (@_outer_Id3 = [x13].[FloorId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
- 已执行 DbCommand ("1"ms) [Parameters=["@_outer_Id='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']"
SELECT (
SELECT COUNT(*)
FROM [Rooms] AS [x4]
WHERE ([x4].[IsDeleted] = 0) AND ([x3].[Id] = [x4].[FloorId])
)
FROM [Floors] AS [x3]
WHERE ([x3].[IsDeleted] = 0) AND (@_outer_Id = [x3].[BuildingId])
- 已执行 DbCommand ("0"ms) [Parameters=["@_outer_Id2='?' (DbType = Guid)"], CommandType='Text', CommandTimeout='30']" =143=]
SELECT [x10].[Id]
FROM [Floors] AS [x10]
WHERE ([x10].[IsDeleted] = 0) AND (@_outer_Id2 = [x10].[BuildingId])
如果您不使用 EF Navigations 属性,而是通过 LINQ to EF 手动连接会怎样?
var ans2 = (from b in dbContext.Buildings
join f in dbContext.Floors on b.Id equals f.BuildingId into fj
from f in fj.DefaultIfEmpty()
join r in dbContext.Rooms on f.Id equals r.FloorId into rj
from r in rj.DefaultIfEmpty()
join ro in dbContext.RoomOccupancies on r.Id equals ro.RoomId
join w in dbContext.WorkGroups on ro.WorkGroupId equals w.Id into wj
from w in wj.DefaultIfEmpty()
where !w.IsFinished && w.StartDate < DateTime.Now
select new BuildingDatableElementDTO() {
BuildingId = b.Id,
Name = b.Name,
FloorCount = fj.Count(),
RoomCount = rj.Count(),
CurrentWorkerCount = wj.Sum(w => w.NumberOfEmployees)
})
.ToList();
I read that there are problems with the aggregates with EF Core 2.1, but I think it shouldn't be a hard task for the ORM to translate this Projection into one query.
你是对的,EF Core 在翻译 GroupBy
和聚合(而且不仅如此)时存在(并且仍然存在 - 目前最新的 v2.2)问题。但不适用于 "shouldn't be a hard task" - 自己尝试将任意表达式树转换为伪 SQL,您很快就会发现这是一项相当复杂的任务。
无论如何,EF Core 查询翻译随着时间的推移而改进,但如前所述,它远非完美。在这种情况下,阻碍因素是嵌套聚合 - sum/count 等的总和。解决方案是展平目标集并应用单个聚合。例如,按如下方式重写 LINQ 查询:
dbContext.Buildings.Select(b => new //BuildingDatableElementDTO()
{
BuildingId = b.Id,
Name = b.Name,
FloorCount = b.Floors.Count(),
// (1)
RoomCount = b.Floors.SelectMany(f => f.Rooms).Count(),
// (2)
CurrentWorkerCount = b.Floors
.SelectMany(f => f.Rooms)
.SelectMany(r => r.RoomOccupancies)
.Select(o => o.WorkGroup)
.Where(w => !w.IsFinished && w.StartDate < DateTime.Now)
.Sum(w => w.NumberOfEmployees),
})
.ToList();
被翻译成单个 SQL(如预期的那样):
SELECT [e].[Id] AS [BuildingId], [e].[Name], (
SELECT COUNT(*)
FROM [Floors] AS [e0]
WHERE ([e0].[IsDeleted] = 0) AND ([e].[Id] = [e0].[BuildingId])
) AS [FloorCount], (
SELECT COUNT(*)
FROM [Floors] AS [e1]
INNER JOIN (
SELECT [e2].[Id], [e2].[FloorId], [e2].[IsDeleted], [e2].[Name]
FROM [Rooms] AS [e2]
WHERE [e2].[IsDeleted] = 0
) AS [t] ON [e1].[Id] = [t].[FloorId]
WHERE ([e1].[IsDeleted] = 0) AND ([e].[Id] = [e1].[BuildingId])
) AS [RoomCount], (
SELECT SUM([f.Rooms.RoomOccupancies.WorkGroup].[NumberOfEmployees])
FROM [Floors] AS [e3]
INNER JOIN (
SELECT [e4].*
FROM [Rooms] AS [e4]
WHERE [e4].[IsDeleted] = 0
) AS [t0] ON [e3].[Id] = [t0].[FloorId]
INNER JOIN (
SELECT [e5].*
FROM [RoomOccupancies] AS [e5]
WHERE [e5].[IsDeleted] = 0
) AS [t1] ON [t0].[Id] = [t1].[RoomId]
INNER JOIN [WorkGroups] AS [f.Rooms.RoomOccupancies.WorkGroup] ON [t1].[WorkgroupId] = [f.Rooms.RoomOccupancies.WorkGroup].[Id]
WHERE (([e3].[IsDeleted] = 0) AND (([f.Rooms.RoomOccupancies.WorkGroup].[IsFinished] = 0) AND ([f.Rooms.RoomOccupancies.WorkGroup].[StartDate] < GETDATE()))) AND ([e].[Id] = [e3].[BuildingId])
) AS [CurrentWorkerCount]
FROM [Building] AS [e]
WHERE [e].[IsDeleted] = 0