Group and Select First From Two Tables Linq

前端 未结 2 1610
庸人自扰
庸人自扰 2021-01-23 16:20

I\'m trying to create a simple query using EFCore, returning the list of people i\'m conversing with, and the last message that was sent between the two of us (pretty much like

2条回答
  •  有刺的猬
    2021-01-23 17:05

    The query you are looking for standardly is expressed in LINQ to Entities (EF) with something like this (no joins, no GroupBy, use navigation properties):

    var query = context.Visitors
        .Select(v => new
        {
            Visitor = v,
            Message = v.VisitorChatMessages
                .OrderByDescending(m => m.CreatedAt)
                .FirstOrDefault()
        });
    

    But here is the trap. EF6 creates quite inefficient SQL query, and EF Core until now produces (again quite inefficient) N + 1 SQL queries.

    But this is changing in EF Core 3.0 in a positive direction! Usually (and still) I don't recommend using the preview (beta) versions of EF Core 3.0, because they are rewriting the whole query translation/processing pipeline, so many things don't work as expected.

    But today I've updated my EF Core test environment to EF Core 3.0 Preview 9 and I'm pleased to see that the above query now nicely translates to the following single SQL query:

      SELECT [v].[Id], [v].[CreatedAt], [v].[CreatedBy], [v].[Email], [v].[Fingerprint], [v].[IP], [v].[IsDeleted], [v].[LastUpdatedAt], [v].[LastUpdatedBy], [v].[Name], [v].[Phone], [t0].[Id], [t0].[CreatedAt], [t0].[CreatedBy], [t0].[IsDeleted], [t0].[IsFromVisitor], [t0].[LastUpdatedAt], [t0].[LastUpdatedBy], [t0].[Message], [t0].[UserId], [t0].[VisitorId]
      FROM [Visitors] AS [v]
      LEFT JOIN (
          SELECT [t].[Id], [t].[CreatedAt], [t].[CreatedBy], [t].[IsDeleted], [t].[IsFromVisitor], [t].[LastUpdatedAt], [t].[LastUpdatedBy], [t].[Message], [t].[UserId], [t].[VisitorId]
          FROM (
              SELECT [c].[Id], [c].[CreatedAt], [c].[CreatedBy], [c].[IsDeleted], [c].[IsFromVisitor], [c].[LastUpdatedAt], [c].[LastUpdatedBy], [c].[Message], [c].[UserId], [c].[VisitorId], ROW_NUMBER() OVER(PARTITION BY [c].[VisitorId] ORDER BY [c].[CreatedAt] DESC) AS [row]
              FROM [ChatMessages] AS [c]
          ) AS [t]
          WHERE [t].[row] <= 1
      ) AS [t0] ON [v].[Id] = [t0].[VisitorId]
    

    Note the beautiful utilization of the ROW_NUMBER() OVER (PARTITION BY ORDER BY) construct. This is the first time EF query translation does that ever. I'm excited. Good job, EF Core team!


    Update: The exact equivalent of your first query (which btw fails with runtime exception in Preview 9)

    from c in context.ChatMessages
    orderby c.CreatedAt descending 
    group c by c.VisitorId  into x
    select x.First()
    

    but with additional information is

    from v in context.Visitors
    from c in v.VisitorChatMessages
        .OrderByDescending(c => c.CreatedAt)
        .Take(1)
    orderby c.CreatedAt descending
    select new
    {
        Visitor = v,
        Message = c
    })
    

    The generated SQL is pretty much the same - just the LEFT OUTER JOIN becomes INNER JOIN and there is additional ORDER BY at the end.

    Looks like that to make this work, it's essential to avoid GroupBy and use GroupJoin (which collection navigation property represents in LINQ to Entities queries) or correlated SelectMany to achieve the desired grouping.

提交回复
热议问题