【问题标题】:Simplify Entity Framework Core query简化 Entity Framework Core 查询
【发布时间】:2022-01-25 19:18:15
【问题描述】:

我不明白如何简化这个请求

var dialogs = await dbContext.UsersDialogs
            .AsNoTracking()
            .Where(x => x.UserId == userId)
            .Select(x => new DialogModel
            {
                Id = x.DialogId,
                Login = x.Dialog.Name,
                Image = x.User.FacialImage,
                IsConfirm = x.Dialog.Messages.OrderBy(x => x.DateCreate).LastOrDefault().IsRead,
                DateTime = x.Dialog.Messages.OrderBy(x => x.DateCreate).LastOrDefault().DateCreate,
                LastMessage = x.Dialog.Messages.OrderBy(x => x.DateCreate).LastOrDefault().Content,
                LastUserId = x.Dialog.Messages.OrderBy(x => x.DateCreate).LastOrDefault().UserId
            })
            .ToListAsync();

它将它转换成这样的请求

SELECT [u].[DialogId] AS [Id], [d].[Name] AS [Login], [u0].[FacialImage] AS [Image], (
      SELECT TOP(1) [m].[IsRead]
      FROM [Messages] AS [m]
      WHERE [d].[Id] = [m].[DialogId]
      ORDER BY [m].[DateCreate] DESC) AS [IsConfirm], (
      SELECT TOP(1) [m0].[DateCreate] 
  FROM [Messages] AS [m0]
      WHERE [d].[Id] = [m0].[DialogId]
      ORDER BY [m0].[DateCreate] DESC) AS [DateTime], (
      SELECT TOP(1) [m1].[Content]
      FROM [Messages] AS [m1]
      WHERE [d].[Id] = [m1].[DialogId]
      ORDER BY [m1].[DateCreate] DESC) AS [LastMessage], (
      SELECT TOP(1) [m2].[UserId]
      FROM [Messages] AS [m2]
      WHERE [d].[Id] = [m2].[DialogId]
      ORDER BY [m2].[DateCreate] DESC) AS [LastUserId]
  FROM [UsersDialogs] AS [u]
  INNER JOIN [Dialogs] AS [d] ON [u].[DialogId] = [d].[Id]
  INNER JOIN [Users] AS [u0] ON [u].[UserId] = [u0].[Id]
  WHERE [u].[UserId] = @__userId_0

我以某种方式优化它?我不想使用 SQL 查询,因为 Linq 对我来说似乎更方便。

【问题讨论】:

  • 在第一次选择中,可以创建一个匿名对象来存储x.Dialog.Messages.OrderBy(x => x.DateCreate).LastOrDefault() and other direct property of x,稍后在选择中创建DialogModel的对象。这应该比您多次生成Messages 表的查询更好。
  • 如果您使用的是 EF Core 5+,这对于 Filtered includes 来说似乎是一个很好的用例
  • TBH 我比我更喜欢 Svyatoslav 的第二种选择。随意将其标记为已接受。

标签: c# linq asp.net-core entity-framework-core


【解决方案1】:

您已创建直接转换为相同 SQL 的查询。您可以通过附加Select 省略重复查询。也不需要AsNoTracking - EF Core 不添加自定义实体。

var dialogs = await dbContext.UsersDialogs
    .Where(x => x.UserId == userId)
    .Select(x => new 
    { 
        UserDialog = x, 
        LastMessage = x.Dialog.Messages.OrderByDescending(x => x.DateCreate).FirstOrDefault()
    })
    .Select(x => new DialogModel
    {
        Id = x.UserDialog.DialogId,
        Login = x.UserDialog.Dialog.Name,
        Image = x.UserDialog.User.FacialImage,
        IsConfirm = x.LastMessage.IsRead,
        DateTime = x.LastMessage.DateCreate,
        LastMessage = x.LastMessage.Content,
        LastUserId = x.LastMessage.UserId
    })
    .ToListAsync();

但上述查询的质量取决于当前 EF Core LINQ 翻译器的质量。因此,添加了另一个变体:

var query =  
    from ud in dbContext.UsersDialogs
    from lm in ud.Dialog.Messages.OrderByDescending(x => x.DateCreate)
       .Take(1).DefaultIfEmpty()
    select new DialogModel
    {
        Id = ud.DialogId,
        Login = ud.Dialog.Name,
        Image = ud.User.FacialImage,
        IsConfirm = lm.IsRead,
        DateTime = lm.DateCreate,
        LastMessage = lm.Content,
        LastUserId = lm.UserId
    };

var dialogs = await query.ToListAsync();

【讨论】:

  • 这不会优化 SQL 查询。
  • @GertArnold,它真的取决于 EF Core 版本和它的实现。添加了在任何情况下都可以使用的变体。
  • 我在 EF core 6 和 SQL Server 中对其进行了测试。替代方案做得更好。
  • linq2db 中,即使在原始消息中,它也会在所有情况下进行优化。
  • 是的,linq2db,不知怎的,很多人一直在使用 EF :)
【解决方案2】:

就LINQ查询而言,您可以通过使用查询语法和let来简化它:

from d in dbContext.UsersDialogs
where d.UserId == userId
let lastMessage = d.Dialog.Messages.OrderBy(d => d.DateCreate).LastOrDefault()
select new DialogModel
{
    Id = d.DialogId,
    Login = d.Dialog.Name,
    Image = d.User.FacialImage,
    IsConfirm = lastMessage.IsRead,
    DateTime = lastMessage.DateCreate,
    LastMessage = lastMessage.Content,
    LastUserId = lastMessage.UserId
}

但这并没有优化 SQL 查询。 EF 为 Messages 表中的每个字段一遍又一遍地生成相同的子查询。在 SQL Server 中,查询计划不会将这些子查询优化到一个分支中。

如果你真的想优化 SQL 查询,你必须这样做:

(
    from d in dbContext.UsersDialogs
    where d.UserId == userId
    select new
    {
        Id = d.DialogId,
        Login = d.Dialog.Name,
        Image = d.User.FacialImage,
        LastMessage = (from d.Dialog.Messages
                       orderby d.DateCreate
                       select new
                       {
                           IsConfirm = d.IsRead
                           d.DateCreate,
                           d.Content,
                           d.UserId
                       }).LastOrDefault()
    }
).AsEnumerable()
.Select(x => new DialogModel
{
    Id = x.DialogId,
    Login = x.Dialog.Name,
    Image = x.User.FacialImage,
    LastMessage.IsConfirm,
    LastMessage.DateCreate,
    LastMessage.Content,
    LastMessage.UserId
})

通过添加AsEnumerable,强制客户端评估查询的最后一部分,EF 生成一个带有一个子查询的查询,该子查询使用ROW_NUMBER() OVER 函数仅获取最后一条消息一次。但是,这样做当然很麻烦。但如果第一个查询受到相当大的性能影响,则可能需要这样做。

【讨论】:

    猜你喜欢
    • 2020-09-11
    • 1970-01-01
    • 1970-01-01
    • 2017-03-19
    • 1970-01-01
    • 1970-01-01
    • 2020-08-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多