LINQ lambda 优化 .NET 8

问题描述 投票:0回答:1

我有一个带有 lambda LINQ 的 EF Core 存储库函数,用于读取主对象及其所有相关对象:

public HomeObj ReadByIdWithObjects(int id)
{
   var home = _context.Instance.Homes.FirstOrDefault(x => x.Home_id == id);

   if (home == null) 
       return null;

   home.Seller = _context.Instance
                         .Sellers
                         .FirstOrDefault(x => x.Seller_id == home.Seller_id);

   home.Address = _context.Instance
                          .Addresses
                          .FirstOrDefault(x => x.Address_Id == home.Address_Id);

   home.Owner = _context.Instance
                        .Owners
                        .FirstOrDefault(x => x.Owner_id == home.Owner_id);

   home.Events = _context.Instance
                         .Events
                         .Where(x => x.Home_id == home.Home_id)
                         .ToList();

   return home;
}

此函数的问题是它读取完整的主对象和完整的相关对象,即使只使用了几个属性。为了优化这一点,我尝试使用

.include()
重写它,认为在一次调用中连接表,然后仅选择所需的内容将比 5 个单独的数据库连接更有效。

新功能:

public HomeObj ReadByIdWithObjects(int id)
{
     var home = context.Instance.Homes
    .Where(x => x.Home_id == id)
    .Include(x => x.Seller)
    .Include(x => x.Owner)
    .Include(x => x.Address)
    .Include(x => x.Events)
    .Select(x => new HomeObj
    {
        Home_id = x.Home_id,
        Number = x.Number,
        Color = x.Color,
        DateBuilt = x.DateBuilt,
        Address = new Address
        {
            Id = x.Address.Id
            UPRN = x.Address.UPRN,
            Address_Address = x.Address.Address_Address,
            Town = x.Address.Town,
            Postcode = x.Address.Postcode
        },
        Owner = new Owner
        {
            Owner_id = x.Owner.Owner_id,
            Owner_Name = x.Owner.Owner_Name
        },
        Events = x.Events,
    }).FirstOrDefault();

 return home;
}

令我惊讶的是,新函数的执行时间要长得多。我调试了它,并尝试获取生成的 SQL,这就是我得到的:

DECLARE @__id_0 int = 2114465;

SELECT [h].[Home_id], [h].[Number], [h].[Color], [h].[DateBuilt], 
[a].[Id], [a].[UPRN], [a].[Address_Address], [a].[Town], [a].[Postcode], 
[o].[Owner_id], [o].[Owner_Name],
[e1].[Event_id], [e1].[Owner_id], [e1].[Event_created], [e1].[Event_text],
[e2].[Event_id], [e2].[Owner_id], [e2].[Event_created], [e2].[Event_text]
FROM [Home] AS [h]
INNER JOIN [Address] AS [a] ON [h].[Address_Id] = [a].[Address_Id]
INNER JOIN [Owner] AS [o] ON [h].[Owner_id] = [h].[Owner_id]
LEFT JOIN [Event] AS [e1] ON [h].[Home_id] = [e1].[Home_id]
LEFT JOIN [Event] AS [e2] ON [h].[Home_id] = [e2].[Home_id]
WHERE [a].[Home_id] = @__id_0
ORDER BY [h].[Home_id], [a].[Address_Id], [o].[Owner_id], [e1].[Event_id], [e2].[Event_id]

几个问题:

  1. 优化是否正确,还是我遗漏了什么?
  2. 我可以看到
    events
    表连接了两次,完全相同,但我不知道为什么?
  3. 底部有一个订单行,即使LINQ不包含任何排序,这会影响性能吗?
  4. 通过优化,我还尝试从 .NET 5 更新到 .NET 8。.NET 8 在 SQL Server 2022 上的性能会更好还是两者不相关?我目前使用 SQL Server 2019。此外,除了
    .contains()
    OPENJSON
    用法之外,.NET 8 中的 LINQ 是否存在任何已知问题? (这里提到:https://github.com/dotnet/efcore/issues/32394
c# sql-server linq entity-framework-core .net-8.0
1个回答
0
投票

有关使用 EF 并避免性能问题、异常和其他令人讨厌的行为的一些技巧:

如果您想要一个具有关联数据的实体,只需立即加载即可。您的第一个示例可以简单地替换为:

 var home = context.Instance.Homes
    .Include(x => x.Seller)
    .Include(x => x.Owner)
    .Include(x => x.Address)
    .Include(x => x.Events)
    .Single(x => x.Home_id == id);

这将生成一个 SQL 语句来加载主页和关联数据。这会产生笛卡尔积,该笛卡尔积可能会有点慢并且资源密集。这可以通过

AsSplitQuery()
在 EF Core 8 中进行优化:

 var home = context.Instance.Homes
    .Include(x => x.Seller)
    .Include(x => x.Owner)
    .Include(x => x.Address)
    .Include(x => x.Events)
    .AsSplitQuery()
    .Single(x => x.Home_id == id);

这将为急切加载的实体生成单独的 SQL 语句,从而删除笛卡尔积结果。这在这样的例子中效果很好。加载多个顶级实体时,尤其是涉及排序和/或分页时,您需要小心

AsSplitQuery

在 EF 中绝对应该避免的一件事是公开集合导航属性的公共设置器。代码如下:

home.Events = _context.Instance
                     .Events
                     .Where(x => x.Home_id == home.Home_id)
                     .ToList();

非常糟糕。使用 EF 实体时,更改跟踪是一项非常重要的功能,如果您将集合设置为新实例,则会中断更改跟踪。在您的主页实体中,您的事件和任何其他基于集合的导航属性应被视为只读引用:

public class Home
{
    // ...

    public virtual ICollection<Event> Events { get; protected set; } = new List<Event>();

}

使 setter 不可访问,并自动将默认状态初始化为新集合,准备好用于新的 Home 实例。 EF 将在读取数据时填充此集合,并防止意外或错误的代码破坏更改跟踪。

最后,你的最后一个例子是所谓的投影。然而,对于投影,您应该定义一个新的具体类(ViewModel 或 DTO)来表示您想要投影到的数据,即使在大多数情况下这看起来像实体类。 投影到实体类的新实例中的主要原因是为了避免混淆哪些 Home 等实例是实际跟踪的实体,哪些实例是分离的,以及可能只是部分填充的实例。如果我有一个接受 Home 的函数,它应该期望获得一个完整的实际跟踪的 Home 实体。如果我想优化读取以填充 Home 和相关数据的简化版本,这应该是 HomeViewModel 或 HomeDTO。这意味着期望针对实体工作的方法与期望投影的方法之间不会产生混淆。使用

Select
进行投影时,您不需要
Include
,并且绝对应该避免将实体与投影混合的任何情况。例如,如果我创建一个 HomeDTO 并想要一个事件列表,我会在 HomeDTO 中创建一个 EventDTO 集合来填充,我不会在 DTO 中存储事件实体引用。

如果启用延迟加载,实体上的序列化之类的事情可能会成为一个巨大的性能杀手,并且当禁用延迟加载并且忘记急切加载

Include
时,可能会导致看似不一致的行为(不完整的数据)。投影到 ViewModels/DTO 有助于避免这些问题,方法是让 EF 填充消费者所需的实例,并构建特定于所需表和列的 SQL 语句。

最新问题
© www.soinside.com 2019 - 2024. All rights reserved.