Aggregate of aggregate with EF Core Linq2Sql

后端 未结 2 1596
南方客
南方客 2021-01-23 13:19

I have a ASP.NET Core 2.2 project with EF Core 2.2 Code-First DB. I have the following entities:

  • Building, which is basically an address with some other importan
相关标签:
2条回答
  • 2021-01-23 14:08

    What if you didn't use EF Navigations properties but used manual joins with 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();
    
    0 讨论(0)
  • 2021-01-23 14:21

    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.

    You are right that EF Core had (and still have - the latest at this time v2.2) problems translating GroupBy and aggregates (and not only). But not for "shouldn't be a hard task" - try converting arbitrary expression tree to pseudo SQL yourself and you'll quickly find that it is quite complicated task.

    Anyway, EF Core query translation improves over the time, but as mentioned, is far from perfect. The showstopper in this case are nested aggregates - sum of sum/count etc. The solution is to flatten the target set and apply single aggregate. For instance, rewriting your LINQ query as follows:

    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();
    

    is translated to a single SQL (as expected):

      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
    
    0 讨论(0)
提交回复
热议问题