简化 Entity Framework 核心查询

Simplify Entity Framework Core query

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

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 对我来说似乎更方便。

您创建的查询直接转换为相同的 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();

就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 table 中的每个字段一遍又一遍地生成相同的子查询。在 SQL 服务器中,查询计划不会将这些子查询优化到一个分支中。

如果您真的想优化 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 函数仅获取最后一条消息一次.但是当然这样做很麻烦。但如果第一个查询遭受相当大的性能损失,则可能有必要。